Skip to content

Authentication

All Mantis API endpoints require authentication. This guide covers the supported authentication methods and flows.

Mantis supports two authentication methods:

  1. JWT Bearer Tokens (recommended for user sessions)
  2. API Keys (recommended for automation and CI/CD)

Exchange your email and password for an access token:

Terminal window
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "your-password"
}

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImN1cnJlbnQifQ...",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "johndoe",
"display_name": "John Doe",
"status": "active",
"roles": ["operator", "user"],
"permissions": ["deployments:create", "deployments:read"],
"created_at": "2024-01-15T10:30:00Z"
}
}

Include the token in the Authorization header with the Bearer prefix:

Terminal window
curl https://your-mantis-server/api/v1/deployments \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImN1cnJlbnQifQ..."

Before your access token expires, refresh it to get a new one:

Terminal window
POST /api/v1/auth/refresh
Cookie: refresh_token=<automatic>

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImN1cnJlbnQifQ...",
"token_type": "Bearer",
"expires_in": 3600,
"user": { ... }
}

A new refresh token is also set as a cookie (refresh token rotation).

Sign out the current session/device: the presented access token is revoked and the caller’s session row is deleted, which cascades to the tokens issued for that session. Other devices keep working. To sign out every OTHER device, use DELETE /api/v1/auth/sessions — it revokes the account’s other sessions and leaves the caller’s own in place.

Terminal window
POST /api/v1/auth/logout
Authorization: Bearer <token>

Response:

{
"message": "Successfully logged out"
}

If 2FA is enabled for your account, the login flow has an additional step.

Terminal window
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "your-password"
}

Response (2FA Required):

{
"requires_2fa": true,
"pending_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImN1cnJlbnQifQ..."
}
Terminal window
POST /api/v1/auth/2fa/verify
Content-Type: application/json
{
"code": "123456",
"pending_token": "<pending_token>"
}

Response (Success):

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImN1cnJlbnQifQ...",
"token_type": "Bearer",
"expires_in": 3600,
"user": { ... }
}

API keys provide long-lived authentication for automation and CI/CD pipelines.

Terminal window
POST /api/v1/auth/api-keys
Authorization: Bearer <jwt-token>
Content-Type: application/json
{
"name": "CI/CD Pipeline",
"description": "Automation token for GitHub Actions",
"expires_in_days": 90,
"scopes": ["deployments:create", "deployments:read"]
}

Response:

{
"api_key": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "CI/CD Pipeline",
"description": "Automation token for GitHub Actions",
"key_prefix": "mnt_a1b2c3d4...",
"scopes": ["deployments:create", "deployments:read"],
"last_used_at": null,
"last_used_ip": null,
"created_at": "2024-01-15T10:30:00Z",
"expires_at": "2024-04-15T10:30:00Z"
},
"full_key": "mnt_a1b2c3d4e5f67890abcdef1234567890abcdef1234567890",
"warning": "Store this key securely. It will not be shown again."
}

Use the API key as a Bearer token (same as JWT):

Terminal window
curl https://your-mantis-server/api/v1/deployments \
-H "Authorization: Bearer mnt_a1b2c3d4e5f67890abcdef1234567890abcdef1234567890"

API keys:

  • Start with the prefix mnt_
  • Don’t expire unless you set expires_in_days
  • Are scoped to the tenant of the user who created them
  • Automatically track last_used_at and last_used_ip
Terminal window
GET /api/v1/auth/api-keys
Authorization: Bearer <jwt-token>

Response:

{
"api_keys": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "CI/CD Pipeline",
"description": "Automation token for GitHub Actions",
"key_prefix": "mnt_a1b2c3d4...",
"scopes": ["deployments:create", "deployments:read"],
"last_used_at": "2024-01-20T15:45:00Z",
"last_used_ip": "203.0.113.42",
"created_at": "2024-01-15T10:30:00Z",
"expires_at": "2024-04-15T10:30:00Z"
}
]
}
Terminal window
DELETE /api/v1/auth/api-keys/{id}
Authorization: Bearer <jwt-token>

Response:

{
"message": "API key revoked successfully"
}

Server-Sent Events (SSE) endpoints — such as GET /api/v1/sse/{resource_type} and GET /api/v1/deployments/{id}/logs — do not accept your regular JWT in an Authorization header. Because EventSource cannot set request headers, SSE connections authenticate with a short-lived, single-use token passed in the ?sse_token= query parameter.

Exchange a valid JWT (or API key) for an SSE token:

Terminal window
POST /api/v1/auth/sse-token
Authorization: Bearer <jwt-token>
Content-Type: application/json
{}

Response:

{
"sse_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCI...",
"expires_in": 60,
"session_id": "b3c1...",
"refresh_token": "..."
}

Pass the token in the ?sse_token= query parameter when opening the connection:

const { sse_token } = await fetch('/api/v1/auth/sse-token', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: '{}',
}).then((r) => r.json());
const eventSource = new EventSource(
`/api/v1/sse/deployments?sse_token=${sse_token}`
);

To renew an SSE session without a full re-login, use the session_id and refresh_token from the original response:

Terminal window
POST /api/v1/auth/sse-token/refresh
Content-Type: application/json
{
"session_id": "b3c1...",
"refresh_token": "..."
}

Get information about the currently authenticated user:

Terminal window
GET /api/v1/auth/me
Authorization: Bearer <token>

Response:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "johndoe",
"display_name": "John Doe",
"status": "active",
"email_verified": true,
"roles": ["operator", "user"],
"permissions": ["deployments:create", "deployments:read", "targets:read"],
"last_login_at": "2024-01-20T08:30:00Z",
"created_at": "2023-06-01T12:00:00Z"
}

Web Applications:

  • Store access tokens in memory (JavaScript variable)
  • Never store tokens in localStorage or sessionStorage
  • Refresh tokens are handled automatically via HTTP-only cookies

Mobile/Desktop Apps:

  • Use secure platform-specific storage (Keychain on iOS, Keystore on Android)
  • Never log token values

CI/CD Pipelines:

  • Use API keys instead of JWT tokens
  • Store API keys in secret management systems (GitHub Secrets, HashiCorp Vault, etc.)
  • Rotate API keys regularly (set expires_in_days)

When creating API keys, use the minimum required scopes:

{
"name": "Read-Only Monitoring",
"scopes": ["deployments:read", "targets:read"]
}

Available scopes match the permission system. See the Permissions guide for the full list.

Error:

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

Causes:

  • Missing Authorization header
  • Token not prefixed with Bearer
  • Malformed token

Error:

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

Solution: Call /auth/refresh to get a new access token.

Error:

{
"error": {
"code": "INVALID_CREDENTIALS",
"message": "Invalid credentials"
}
}

Causes:

  • Wrong email or password
  • Account is suspended or inactive
  • API key is expired or revoked

Error:

{
"error": {
"code": "TWO_FACTOR_REQUIRED",
"message": "Two-factor authentication required"
}
}

Cause: You’re using a pending_token on a regular endpoint. Complete the 2FA flow first by calling /auth/2fa/verify.

class MantisClient {
private accessToken: string | null = null;
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async login(email: string, password: string): Promise<void> {
const response = await fetch(`${this.baseUrl}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
credentials: 'include', // Include cookies for refresh token
});
if (!response.ok) {
throw new Error('Login failed');
}
const data = await response.json();
this.accessToken = data.access_token;
}
async listDeployments(): Promise<any> {
const response = await fetch(`${this.baseUrl}/api/v1/deployments`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
});
return response.json();
}
}
// Usage
const client = new MantisClient('https://mantis.example.com');
await client.login('user@example.com', 'password');
const deployments = await client.listDeployments();
import requests
from typing import Optional
class MantisClient:
def __init__(self, base_url: str):
self.base_url = base_url
self.access_token: Optional[str] = None
self.session = requests.Session()
def login(self, email: str, password: str) -> None:
response = self.session.post(
f"{self.base_url}/api/v1/auth/login",
json={"email": email, "password": password}
)
response.raise_for_status()
data = response.json()
self.access_token = data["access_token"]
def list_deployments(self) -> list:
response = self.session.get(
f"{self.base_url}/api/v1/deployments",
headers={"Authorization": f"Bearer {self.access_token}"}
)
response.raise_for_status()
return response.json()
# Usage
client = MantisClient("https://mantis.example.com")
client.login("user@example.com", "password")
deployments = client.list_deployments()
#!/bin/bash
set -e
MANTIS_URL="https://mantis.example.com"
API_KEY="mnt_your_api_key_here"
# Create deployment
curl -X POST "$MANTIS_URL/api/v1/deployments" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"solution_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}'