Skip to content

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.

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 on 401, 403 and 503: what a client should do next
  • details - Optional additional context

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.

ValueMeaningClient behaviour
refresh_and_retryThe access token expiredRefresh, replay the request once
reauthenticateCredentials revoked/expired, or 2FA neededSend the user through sign-in again
terminalForbidden, or the token is invalidDo not retry; surface the error
retry_laterThe service is unavailableBack off and retry
Status CodeMeaningUsage
400Bad RequestValidation errors, malformed requests
401UnauthorizedMissing or invalid authentication
403ForbiddenInsufficient permissions, access denied
404Not FoundResource doesn’t exist
408Request TimeoutAn upstream operation exceeded its time budget
409ConflictDuplicate resource, constraint violation
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnexpected server-side error
503Service UnavailableExternal service (database, queue) unavailable

HTTP Status: 401

Description: General authentication failure.

Example:

{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required"
}
}

Causes:

  • No Authorization header provided
  • Empty authorization header

Resolution: Include the Authorization: Bearer <token> header in your request.


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

HTTP Status: 401

Description: JWT access token has expired.

Example:

{
"error": {
"code": "TOKEN_EXPIRED",
"message": "Token expired"
}
}

Resolution:

  • Call POST /auth/refresh to get a new access token
  • Implement automatic token refresh in your client

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

HTTP Status: 401

Description: The Authorization header is missing.

Example:

{
"error": {
"code": "MISSING_AUTH_HEADER",
"message": "Missing authorization header"
}
}

Resolution: Add the header:

Terminal window
Authorization: Bearer <your-token>

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...

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.

These share the same envelope and status as the codes above:

CodeMeaningResolution
TOKEN_REVOKEDThe access token was revoked — every request after a logout returns thisLog in again; do not retry with the same token
REFRESH_TOKEN_EXPIREDThe refresh token has expiredLog in again
PENDING_LOGIN_EXPIREDThe short-lived pending_token from login expired before 2FA was completedStart the login flow again
SSE_SESSION_NOT_FOUNDThe SSE session id is unknown to the serverRequest a fresh SSE token and reconnect
SSE_SESSION_EXPIREDThe SSE session outlived its lifetimeRequest a fresh SSE token and reconnect
SSE_TOKEN_EXPIREDThe single-use SSE token expired before it was presentedRequest a fresh SSE token and reconnect
SSE_TOKEN_REPLAYEDAn SSE token was presented twice — the ordinary reconnect failure, tokens are single-useRequest a new token for every reconnect rather than reusing one
SSE_TOKEN_MISMATCHThe SSE token does not belong to the session it was presented forRequest a fresh SSE token and reconnect

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.


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

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.


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.


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.

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

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

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: true parameter (if you have override permission)

Three things return 429, with different bodies, so handle them by status code (429) rather than by parsing a specific JSON shape.

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: 1
X-RateLimit-Remaining: 0

Authentication 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: 900
X-RateLimit-Remaining: 0
Content-Type: application/json

Resolution:

  • Honor the Retry-After header before retrying
  • Implement exponential backoff in your client
  • Reduce request frequency
  • Contact your administrator if you need higher general limits

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.

Raised while syncing a Git-backed storage location.

CodeHTTP StatusMeaningResolution
GIT_AUTH_FAILED400The configured credentials were rejected by the remoteRe-check the credential attached to the repo
GIT_INVALID_TARGET400The remote host could not be reachedCheck the host, DNS and egress from Mandible
GIT_REF_NOT_FOUND400The configured branch, tag or commit does not existPoint the storage location at an existing ref
GIT_SYNC_TIMEOUT408The clone or fetch exceeded its time budgetRetry; check remote reachability and repo size

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

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

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

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

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

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.

  1. Check token format:

    Terminal window
    # Decode JWT (without verification) to inspect claims
    echo "eyJhbGciOiJFUzI..." | cut -d'.' -f2 | base64 -d 2>/dev/null | jq
  2. Verify token expiration:

    const token = 'eyJhbGci...';
    const payload = JSON.parse(atob(token.split('.')[1]));
    console.log('Expires at:', new Date(payload.exp * 1000));
  3. Test with curl:

    Terminal window
    curl -v https://mantis.example.com/api/v1/auth/me \
    -H "Authorization: Bearer $TOKEN"

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 requests
from 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()