Authentication
Authenticating against the M&NTIS APIs
The M&NTIS REST APIs (including the Scenario API) are protected by OpenID Connect (Keycloak). Every request must carry a valid access token as a Authorization: Bearer <token> header, plus the context headers that identify which workspace/organization the call applies to.
Endpoints
For a deployment on domain <your-domain> (e.g. mantis-platform.io):
| Purpose | URL |
|---|---|
| API base (Scenario) | https://app.<your-domain>/api/scenario/lab |
| OpenID Connect issuer | https://id.<your-domain>/realms/mantis |
| OIDC discovery document | https://id.<your-domain>/realms/mantis/.well-known/openid-configuration |
| Token endpoint | https://id.<your-domain>/realms/mantis/protocol/openid-connect/token |
The discovery document lists the exact token/authorization endpoints and the scopes supported by your deployment.
1. Obtain an access token
Log in once with the mantis CLI, then exchange the refresh token it stores for access tokens from your own code. The client id is frontend: it is a public client, identical for every user, and not a secret.
mantis user login --domain mantis-platform.io # opens a browser, once
mantis user organization # prints your organization and workspace idsThe login stores an offline refresh token in ~/.config/mantis/config.yml, under profiles.<domain>.refresh_token. Treat it like a password — on its own it is enough to mint access tokens. That token and the two ids are what the rest of this page expects in your shell:
export REFRESH_TOKEN="<profiles.<domain>.refresh_token, from ~/.config/mantis/config.yml>"
export ORGANIZATION_ID="<the organization id printed above>"
export WORKSPACE_ID="<the workspace id printed above>"
export TOKEN="<the access token obtained just below>"Exchange the refresh token whenever you need a fresh access token:
curl -X POST "https://id.mantis-platform.io/realms/mantis/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=frontend" \
-d "refresh_token=$REFRESH_TOKEN"
# -> {"access_token": "...", "expires_in": ..., "refresh_token": "...", ...}import requests
OIDC = "https://id.mantis-platform.io/realms/mantis/protocol/openid-connect/token"
def get_access_token(refresh_token: str) -> str:
resp = requests.post(
OIDC,
data={
"grant_type": "refresh_token",
"client_id": "frontend",
"refresh_token": refresh_token,
},
)
resp.raise_for_status()
return resp.json()["access_token"]Access tokens are short-lived (a few minutes), so exchange on demand rather than storing one. The refresh token itself carries no expiry date: it keeps working as long as it is used, and dies after an idle period configured on the realm — 30 days with Keycloak's default settings. Once it stops working the exchange answers 400 invalid_grant: run mantis user login again and take the new refresh token. The mantis CLI performs exactly this exchange (mantis_api_client/mantis_api_client/oidc.py).
The login already asks for the scopes the API expects — openid, scenario:run and groups — so a token obtained this way is accepted by the scenario-launching endpoints with nothing more to configure.
For an unattended production integration
Do not build on a refresh token tied to somebody's personal account: it inherits their lifecycle, cannot be rotated independently, and attributes every action to them. Ask your M&NTIS administrator for a dedicated OIDC client and a service account that belongs to the workspace, carrying the openid scenario:run groups scopes.
2. Call the API with the token
Pass the token as a Bearer header, along with the ids printed by mantis user organization:
| Header | Value | Required |
|---|---|---|
Authorization | Bearer <access_token> | always |
X-Workspace-Id | your workspace id | on the scenario endpoints |
X-Organization-Id | your organization id | as soon as an endpoint resolves permissions, the lab list among them |
Send both ids together, as the mantis CLI does. An endpoint that resolves permissions needs the organization id to do so, and answers 500 when only the workspace header is present.
curl "https://app.mantis-platform.io/api/scenario/lab/version" \
-H "Authorization: Bearer $TOKEN"
curl "https://app.mantis-platform.io/api/scenario/lab/scenario/" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "X-Organization-Id: $ORGANIZATION_ID"import requests
BASE = "https://app.mantis-platform.io/api/scenario/lab"
def api_headers(token: str, workspace_id: str, organization_id: str) -> dict:
return {
"Authorization": f"Bearer {token}",
"X-Workspace-Id": workspace_id,
"X-Organization-Id": organization_id,
}
headers = api_headers(TOKEN, WORKSPACE_ID, ORGANIZATION_ID)
resp = requests.get(f"{BASE}/scenario/", headers=headers)
resp.raise_for_status()
print(resp.json())Some scenario-launching endpoints additionally require the token to carry specific scopes (e.g. scenario:run). These are shown per-operation in the API reference and enforced server-side.
3. Public-access links (advanced)
When creating a lab with public_access_enabled: true, the client performs an OIDC token exchange to mint a token dedicated to the public link, and passes it in the request body as public_access_config. See exchange_token_public_access() in mantis_authz and create_lab_scenario() in the reference client for the exact flow.
Next step
Head to the Quickstart to launch a lab end-to-end with these credentials.

