Appearance
Security and platform API reference
What this page covers
This page documents every endpoint in seven areas of the mmune API: authentication (/api/v1/auth), audit log (/api/v1/audit), Guardian security scans (/api/v1/guardian), compliance posture (/api/v1/compliance), masking policies (/api/v1/masking), licensing (/api/v1/license) and Passive Audit reports (/api/v1/reports). For each one you get the method, full path, purpose, required permission, input fields, response fields, status codes and a curl example. Worked Python and TypeScript examples for the common tasks are at the end of the page.
Prerequisites: a running mmune installation reachable at a base URL (this page uses https://mmune.example.com), and an account that can log in. Read API overview first for the conventions that apply to every endpoint: Bearer authentication, the error envelope, request IDs, rate limiting and pagination. This page only repeats the parts that differ per endpoint.
Endpoint index
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/auth/login | Exchange email and password for an access token, or an MFA challenge |
| POST | /api/v1/auth/refresh | Exchange an unexpired token for a new access token |
| GET | /api/v1/auth/me | Read the caller's profile |
| POST | /api/v1/auth/change-password | Change the caller's password |
| POST | /api/v1/auth/logout | Revoke the caller's current token |
| GET | /api/v1/auth/validate-token | Check that a token is valid |
| POST | /api/v1/auth/mfa/setup | Start TOTP enrollment |
| POST | /api/v1/auth/mfa/enable | Finish TOTP enrollment and receive backup codes |
| POST | /api/v1/auth/mfa/disable | Turn MFA off (needs the password) |
| GET | /api/v1/auth/mfa/status | Report whether MFA is enabled |
| POST | /api/v1/auth/mfa/verify-login | Second login step for MFA accounts |
| GET | /api/v1/audit/events | List audit log events with filters (admin) |
| GET | /api/v1/audit/events/{event_id} | Fetch one audit event (admin) |
| POST | /api/v1/guardian/scan | Start a sensitive-data scan job (admin) |
| GET | /api/v1/guardian/status/{job_id} | Poll a scan job and read its results |
| GET | /api/v1/guardian/scan/status | Summary of the last completed scan |
| GET | /api/v1/compliance/posture | Latest score per compliance framework |
| GET | /api/v1/compliance/history | Recent scan runs |
| GET | /api/v1/compliance/findings | Per-finding detail for the latest scan |
| POST | /api/v1/masking/generate | Build a masking policy from a scan |
| GET | /api/v1/masking | List masking policies |
| POST | /api/v1/masking/apply | Apply a policy to rows you supply |
| GET | /api/v1/license/status | Current license state, usage and expiry |
| POST | /api/v1/license | Apply a vendor-signed license token (admin) |
| GET | /api/v1/license/attestation | Signed usage report for renewal (admin) |
| POST | /api/v1/reports/audit | Start Passive Audit report generation |
| GET | /api/v1/reports/status/{report_id} | Poll report generation |
| GET | /api/v1/reports/impact-cost-config | Read impact cost assumptions |
| PUT | /api/v1/reports/impact-cost-config | Set or clear impact cost assumptions |
| GET | /api/v1/reports/{report_id} | Download the finished report as HTML |
How access is decided on these routers
Access control happens in layers, and a request has to pass all of them.
- Every route requires a valid Bearer JWT with the
readpermission (or theadminrole), with these exceptions:POST /api/v1/auth/login,POST /api/v1/auth/mfa/verify-login,POST /api/v1/auth/refreshandGET /healthneed no token, and the probe heartbeat (POST /api/v1/probes/heartbeat) and discovery-agent calls (/api/v1/discovery-agents/{agent_id}/...) authenticate with their own key. A missing or bad token gets 401Not authenticatedorInvalid token. - The role
adminis required forPOST /api/v1/license,GET /api/v1/license/attestationandPOST /api/v1/guardian/scan. A non-admin gets 403 with body{"detail": {"code": "insufficient_permission", "required": "admin", "message": "Your role cannot do this. Ask an administrator."}}. - Any mutating method (POST, PUT, PATCH, DELETE) requires the
writepermission or theadminrole, except for a short list a read-only user may call. On these routers that list isPOST /auth/change-password,POST /auth/logout,POST /auth/mfa/setup,POST /auth/mfa/enableandPOST /auth/mfa/disable. A failure returns 403 with"required": "write". - Individual endpoints can add a further check, shown below as "Route check". It returns 403 with a plain string detail such as
Permission 'write' requiredorRole 'admin' required.
Accounts are managed by your administrator. Service accounts and per-agent API keys are not offered yet, so integrations log in as a regular user account. Every Bearer token must be sent as Authorization: Bearer <TOKEN>.
Pagination, where an endpoint has it, uses page (1-based, default 1) and page_size (default 10, silently clamped to 1 through 100). Both are described in API overview.
Auth router: /api/v1/auth
Access tokens are JWTs. They are valid for 30 minutes by default, so expires_in is normally 1800.
POST /api/v1/auth/login
Exchanges an email and password for an access token. If the account has MFA enabled, it returns a short-lived MFA challenge instead and you finish with POST /api/v1/auth/mfa/verify-login.
Auth: none (public).
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email format) | yes | Account email. |
password | string | yes | Account password. |
Response 200 without MFA:
| Field | Type | Description |
|---|---|---|
access_token | string | JWT for the Authorization header. |
token_type | string | Always bearer. |
expires_in | number | Lifetime in seconds. |
user | object | id, email, name, role, permissions. |
Response 200 with MFA enabled:
| Field | Type | Description |
|---|---|---|
mfa_required | boolean | Always true. |
mfa_token | string | Purpose-restricted JWT that only /mfa/verify-login accepts. |
expires_in | number | Lifetime in seconds, 300. |
Status codes: 200 success; 401 Incorrect email or password (with WWW-Authenticate: Bearer); 422 malformed body; 429 Too many failed login attempts. Try again in a minute. with header Retry-After: 60, returned after 5 failed attempts within 60 seconds for the same client IP and email pair (failed MFA codes count toward the same limit); 503 Password store temporarily unavailable. Failed and successful logins are written to the audit log.
bash
curl -s -X POST https://mmune.example.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "<PASSWORD>"}'POST /api/v1/auth/refresh
Exchanges a still-valid token for a new access token. There is no separate refresh token type: the login response contains only access_token, and the field named refresh_token takes that same unexpired access token. The token you send is revoked after a successful call, so each token can be exchanged once. An expired token cannot be refreshed; log in again.
Auth: none (public).
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
refresh_token | string | yes | A currently valid access token. |
Response 200: access_token (string), token_type (bearer), expires_in (number, seconds). The new token carries the same sub, email, name, role and permissions as the old one.
Status codes: 200 success; 401 Invalid refresh token (bad signature or expired), 401 Refresh token has already been used or revoked; 422 malformed body.
bash
curl -s -X POST https://mmune.example.com/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "<TOKEN>"}'GET /api/v1/auth/me
Returns the caller's profile.
Auth: any valid token with read.
Response 200: id, email, name, role (strings), permissions (array of strings), is_active (boolean).
Status codes: 200; 401.
bash
curl -s https://mmune.example.com/api/v1/auth/me -H "Authorization: Bearer <TOKEN>"POST /api/v1/auth/change-password
Changes the caller's password. The new password is stored durably, so it survives restarts and applies on every replica.
Auth: any valid token with read (self-service).
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
current_password | string | yes | The password in force now. |
new_password | string | yes | The replacement password. |
Response 200: {"message": "Password changed successfully"}.
Status codes: 200; 401 Current password is incorrect; 503 Password store temporarily unavailable.
bash
curl -s -X POST https://mmune.example.com/api/v1/auth/change-password \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"current_password": "<OLD_PASSWORD>", "new_password": "<NEW_PASSWORD>"}'POST /api/v1/auth/logout
Revokes the Bearer token used for the call, so it cannot be used again even though it has not expired. The event is audit logged.
Auth: any valid token with read (self-service). No body.
Response 200: {"message": "Logged out successfully"}. Status codes: 200; 401.
bash
curl -s -X POST https://mmune.example.com/api/v1/auth/logout -H "Authorization: Bearer <TOKEN>"GET /api/v1/auth/validate-token
Confirms the Bearer token is valid and returns who it belongs to.
Auth: any valid token with read.
Response 200: valid (always true), user_id, email, role. Status codes: 200; 401 (Token has been revoked, MFA verification required for an unfinished MFA token, Invalid token, which is also what an expired token returns).
bash
curl -s https://mmune.example.com/api/v1/auth/validate-token -H "Authorization: Bearer <TOKEN>"POST /api/v1/auth/mfa/setup
Starts TOTP enrollment by generating and storing a pending secret. MFA is not enforced at login until /mfa/enable succeeds. Calling this again before enabling replaces the pending secret.
Auth: any valid token with read (self-service). No body.
Response 200: secret (string, the TOTP secret for manual entry) and qr_code_base64 (string, a base64-encoded PNG QR code for an authenticator app).
Status codes: 200; 401.
bash
curl -s -X POST https://mmune.example.com/api/v1/auth/mfa/setup -H "Authorization: Bearer <TOKEN>"POST /api/v1/auth/mfa/enable
Finishes enrollment by proving the authenticator app works. Returns the only copy of the backup codes.
Auth: any valid token with read (self-service).
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | Current code from the authenticator app. One 30-second step either side is accepted. |
Response 200: backup_codes (array of 10 strings, 8 uppercase hex characters each). They are shown once. Each works once in place of a TOTP code at /mfa/verify-login.
Status codes: 200; 400 No MFA enrollment in progress. Call /mfa/setup first., 400 Invalid verification code; 401.
bash
curl -s -X POST https://mmune.example.com/api/v1/auth/mfa/enable \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"token": "123456"}'POST /api/v1/auth/mfa/disable
Turns MFA off. A valid session is not enough; the password is required again.
Auth: any valid token with read (self-service).
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
password | string | yes | The caller's current password. |
Response 200: {"message": "MFA disabled"}. Status codes: 200; 401 Incorrect password.
bash
curl -s -X POST https://mmune.example.com/api/v1/auth/mfa/disable \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"password": "<PASSWORD>"}'GET /api/v1/auth/mfa/status
Reports whether MFA is enabled for the caller.
Auth: any valid token with read. Response 200: {"enabled": true} or {"enabled": false}. Status codes: 200; 401.
bash
curl -s https://mmune.example.com/api/v1/auth/mfa/status -H "Authorization: Bearer <TOKEN>"POST /api/v1/auth/mfa/verify-login
Second step of login for an MFA account. Exchanges the mfa_token from /login plus a TOTP code or an unused backup code for a real access token. The mfa_token is single use and expires after 5 minutes.
Auth: none (public). The mfa_token in the body is the credential.
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
mfa_token | string | yes | Value returned by /login. |
token | string | yes | TOTP code, or a backup code. |
Response 200: same shape as the non-MFA /login response (access_token, token_type, expires_in, user).
Status codes: 200; 400 MFA is not enabled for this account; 401 MFA session expired or invalid — please log in again, 401 Invalid MFA session, 401 Invalid MFA code, 401 Account not found.
bash
curl -s -X POST https://mmune.example.com/api/v1/auth/mfa/verify-login \
-H "Content-Type: application/json" \
-d '{"mfa_token": "<MFA_TOKEN>", "token": "123456"}'Audit router: /api/v1/audit
A read-only view of the audit trail. Entries are created for authentication events and for changes made through the API.
GET /api/v1/audit/events
Lists audit events, newest first. Admin only.
Auth: Route check Role 'admin' required (non-admin gets 403 with that string).
Query parameters:
| Name | Type | Required | Description |
|---|---|---|---|
page | integer | no | 1-based page number. Default 1. |
page_size | integer | no | Items per page. Default 10, clamped to 1 through 100. |
event_type | string | no | One audit event type: user_login, user_logout, user_login_failed, password_change, mfa_enabled, mfa_disabled, access_granted, access_denied, permission_changed, role_assigned, role_removed, data_read, data_created, data_updated, data_deleted, data_exported, data_imported, migration_started, migration_completed, migration_failed, migration_paused, migration_resumed, migration_cancelled, schema_discovered, schema_analyzed, schema_mapped, schema_validated, config_changed, system_setting_changed, integration_configured, security_alert, encryption_key_rotated, certificate_expired, suspicious_activity, compliance_check, audit_requested, data_retention_applied, gdpr_request, system_started, system_stopped, backup_created, backup_restored or error_occurred. |
user_id | string | no | Match the event's user_id. Login events record the email in username and leave user_id empty, so filter those by scanning username in the results. |
resource_type | string | no | Match resource_type, for example auth or api. |
severity | string | no | One of info, low, medium, high, critical. |
start_time | datetime (ISO 8601) | no | Only events at or after this time. |
end_time | datetime (ISO 8601) | no | Only events at or before this time. |
Response 200:
| Field | Type | Description |
|---|---|---|
events | array | Event objects, see below. |
page | integer | Page returned. |
page_size | integer | Effective page size after clamping. |
There is no total count. Fetch the next page until events is empty or shorter than page_size.
Event object fields: event_id, event_type, severity, timestamp (ISO 8601), user_id, username, ip_address, resource_type, resource_id, action, status, details (object), metadata (object).
Status codes: 200; 400 for an unknown event_type or severity value (error envelope with type: validation_error); 401; 403.
bash
curl -s "https://mmune.example.com/api/v1/audit/events?event_type=user_login_failed&severity=medium&page=1&page_size=50" \
-H "Authorization: Bearer <TOKEN>"GET /api/v1/audit/events/
Fetches one audit event. Admin only.
Auth: Route check Role 'admin' required.
Path parameters: event_id (string, required), the event_id field of an event.
Response 200: one event object with the fields listed above. Status codes: 200; 401; 403; 404 Audit event not found: <id>.
bash
curl -s https://mmune.example.com/api/v1/audit/events/<EVENT_ID> -H "Authorization: Bearer <TOKEN>"Guardian router: /api/v1/guardian
Guardian scans the schema mmune already holds for columns that look like personal or regulated data. Scans run as background jobs.
POST /api/v1/guardian/scan
Starts a scan job and returns immediately.
Auth: admin role. A non-admin gets 403 with required: admin and no job is created.
Request body (JSON), all fields optional:
| Field | Type | Default | Description |
|---|---|---|---|
target | string | "all" | Stored in the job metadata. |
metadata | object | {} | Schema to scan, shaped {"tables": [{"name": "t", "columns": [{"name": "c", "type": "TEXT"}]}]}. If tables is missing or empty, the scan uses the schema persisted during integration introspection. |
samples | object | {} | Optional sample values keyed "table.column", each a list. Only the first 5 values per key are used. |
Response 200: {"job_id": "<uuid>", "status": "PENDING"}.
Status codes: 200; 401; 403; 422 malformed body. A scan that runs longer than 1800 seconds is marked FAILED.
bash
curl -s -X POST https://mmune.example.com/api/v1/guardian/scan \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"target": "all"}'GET /api/v1/guardian/status/
Returns the state, logs and results of a scan job. Poll until status is COMPLETED or FAILED.
Auth: any valid token with read.
Path parameter: job_id (string, required). Query parameter: cursor (integer, default 0, minimum 0), the next_cursor from the previous call, so you receive only new log lines.
Response 200:
| Field | Type | Description |
|---|---|---|
job_id | string | The job ID. |
status | string | PENDING, RUNNING, COMPLETED or FAILED. |
logs | array of strings | Log lines from cursor onward. |
next_cursor | integer | Pass as cursor on the next call. |
results | object or null | Scan report once complete: summary (total_fields, flagged_fields, risk_score), findings (array), scan_degraded (boolean, true when no LLM batch succeeded and only the pattern checks ran). See GET /api/v1/compliance/findings for the findings. |
error | string or null | Failure reason when status is FAILED. |
Status codes: 200; 401; 404 Job not found. Job ids expire; use the compliance endpoints for stored results.
bash
curl -s "https://mmune.example.com/api/v1/guardian/status/<JOB_ID>?cursor=0" \
-H "Authorization: Bearer <TOKEN>"GET /api/v1/guardian/scan/status
Summary of the most recent completed scan.
Auth: any valid token with read.
Response 200: findings_count (integer, the scan's flagged field count, 0 if no scan exists), last_scan (ISO 8601 string or null).
Status codes: 200; 401.
bash
curl -s https://mmune.example.com/api/v1/guardian/scan/status -H "Authorization: Bearer <TOKEN>"Compliance router: /api/v1/compliance
Read-only. These endpoints report automated monitoring over controls mmune can observe. The response says so itself: the disclaimer field reads Automated posture monitoring over observable controls — not a certification audit. Do not present the scores as certification results.
All three endpoints need a valid token with read and have no further route check.
GET /api/v1/compliance/posture
Returns the latest assessment for each framework plus the auto-scan scheduler state. There is no single numeric score field. Compute a score as controls_passed / controls_total per framework.
Response 200:
| Field | Type | Description |
|---|---|---|
frameworks | object | Keys among GDPR, HIPAA, SOC2, PCI-DSS. A framework with no stored assessment is absent. |
frameworks.<name>.framework | string | Framework name. |
frameworks.<name>.status | string | no_gaps, warning, gaps_found or not_assessed. |
frameworks.<name>.controls_total | integer | Number of controls evaluated (5 per framework). |
frameworks.<name>.controls_passed | integer | Controls with status pass. |
frameworks.<name>.controls | array | Items with id, title, status (pass, warning, fail, not_assessed) and evidence (string). |
frameworks.<name>.scan_job_id | string or null | Scan the assessment followed. |
frameworks.<name>.assessed_at | string or null | ISO 8601 time of assessment. |
auto_scan | object | enabled, interval_hours, scan_running, next_scheduled_at, last_trigger_reason. If the scheduler cannot be read this is {"enabled": false}. |
disclaimer | string | Fixed text described above. |
Status codes: 200; 401.
bash
curl -s https://mmune.example.com/api/v1/compliance/posture -H "Authorization: Bearer <TOKEN>"GET /api/v1/compliance/history
Lists recent scan runs, newest first.
Query parameters: limit (integer, default 20, range 1 through 100).
Response 200: {"scans": [...]} where each item has job_id, created_at (ISO 8601 or null), total_fields, flagged_fields, risk_score (number), degraded (boolean).
Status codes: 200; 401; 422 if limit is out of range.
bash
curl -s "https://mmune.example.com/api/v1/compliance/history?limit=10" -H "Authorization: Bearer <TOKEN>"GET /api/v1/compliance/findings
Per-finding detail for the newest scan.
Query parameters: limit (integer, default 200, range 1 through 1000), the maximum number of findings returned.
Response 200:
| Field | Type | Description |
|---|---|---|
scan | object or null | Latest scan metadata (job_id, created_at, total_fields, flagged_fields, risk_score, degraded). null if no scan has run. |
findings | array | Items with id, table, column, pii_types (array of strings), sensitivity (HIGH, MEDIUM or LOW), suggested_action, reasoning. |
summary | object | high, medium, low, total, counted over all findings of the scan, not just the rows returned under limit. |
Status codes: 200; 401; 422 if limit is out of range.
bash
curl -s "https://mmune.example.com/api/v1/compliance/findings?limit=500" -H "Authorization: Bearer <TOKEN>"Masking router: /api/v1/masking
A masking policy is a list of per-column rules derived from the findings of one Guardian scan. Applying a policy transforms rows you send; mmune does not rewrite data in your databases.
POST /api/v1/masking/generate
Builds and stores a policy from a finished scan, one rule per finding.
Auth: Route check Permission 'write' required (admin passes), plus the general write check.
Request body (JSON):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
scan_job_id | string | yes | none | job_id of a completed Guardian scan. |
project_id | string | no | "system" | Project the policy is stored under. |
Response 200: policy_id (string) and rules (array). Each rule has table, column, pii_type, sensitivity, action (the suggested action in upper case, REDACT if none) and method. method is hash when the suggested action is HASH, partial when it is MASK, and redact otherwise.
Status codes: 200; 400 with detail set to a list of error strings when scan_job_id is unknown or policy generation fails; 401; 403; 422 malformed body.
bash
curl -s -X POST https://mmune.example.com/api/v1/masking/generate \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"scan_job_id": "<JOB_ID>"}'GET /api/v1/masking
Lists stored policies, newest first.
Auth: any valid token with read. Query parameters: page (default 1), page_size (default 10, clamped to 1 through 100).
Response 200: policies (array of policy_id, scan_job_id, project_id, rules), total (integer, all policies), page, page_size.
Status codes: 200; 401.
bash
curl -s "https://mmune.example.com/api/v1/masking?page=1&page_size=20" -H "Authorization: Bearer <TOKEN>"POST /api/v1/masking/apply
Applies a stored policy to rows in the request and returns the masked copies. A rule only acts on a row if the row has a key equal to the rule's column. null values are left as they are.
Auth: Route check Permission 'write' required, plus the general write check.
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
policy_id | string | yes | A policy_id from /generate or the list. |
rows | array of objects | yes | Rows to mask, each a JSON object keyed by column name. |
Methods applied:
| Method | Result |
|---|---|
redact | The value becomes null. |
hash | A one-way hash of the value, shown as 16 hex characters. |
partial | Values of 5 or more characters keep the first 2 and last 2 characters with * between them. Shorter values become all *. |
Response 200: {"rows": [...]} in the same order as the input.
Status codes: 200; 401; 403; 404 Policy not found; 422 malformed body.
bash
curl -s -X POST https://mmune.example.com/api/v1/masking/apply \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"policy_id": "<POLICY_ID>", "rows": [{"ssn": "123-45-6789", "email": "a@example.com"}]}'License router: /api/v1/license
Enforcement is fully offline. A license is a vendor-signed Ed25519 JWT checked against public keys built into the image. Tokens are read, in order of precedence, from the database (applied through this API), the file at MMUNE_LICENSE_FILE (default /var/lib/mmune/license/mmune.license), then the MMUNE_LICENSE environment variable. The first one that verifies and satisfies its binding wins. The state is re-evaluated at startup and every hour.
License states
| State | When it occurs | Effect |
|---|---|---|
valid | A token verifies and the effective time is at or before exp. | Everything runs. New integrations can be registered until systems_used reaches max_systems. At the cap, registration returns 403 license_cap_exceeded. |
grace | Past exp but within grace_days after it. | Same as valid: registration (within the cap) and monitoring continue. grace_days_remaining in the status counts down. A warning event is emitted on entering grace. |
expired | Past exp plus grace_days. | New registrations are refused with 403 license_blocked. The hourly check stops the watchdog, AutoPilot and the reconciliation runner. The HTTP API itself, including login, reads and this license router, keeps answering. |
missing | No token found in the database, file or environment. | max_systems is 0. Registrations are refused with 403 license_blocked. Watchdog and AutoPilot do not start. Apply a token to recover. |
invalid | A token exists but none verify, or a bound license names a different install_id. | max_systems is 0, reason explains why. Same effect as missing. |
Notes that apply to all states. Integrations already in the registered state are exempt from the cap check, so a restart at or over the cap still boots, and over-cap integrations keep being monitored; the cap only blocks net-new registrations. The registration endpoint that enforces this is POST /api/v1/integrations/discovered/{discovered_id}/register. A renewal applied during expired or grace restarts monitoring in the same request, with no restart of the service.
GET /api/v1/license/status
Current license state, usage and expiry. The call also lines the running monitoring services up with the computed state if the hourly check has not noticed a natural expiry yet.
Auth: any valid token with read.
Response 200:
| Field | Type | Description |
|---|---|---|
state | string | One of valid, grace, expired, missing, invalid. |
license_id | string or null | License jti. |
customer | string or null | Customer slug. |
customer_name | string or null | Display name. |
max_systems | integer | Licensed system cap. 0 means none allowed. |
systems_used | integer | Registered systems counted against the cap. |
expires_at | string or null | ISO 8601 expiry. |
grace_until | string or null | ISO 8601 end of grace. |
grace_days_remaining | number or null | Days until grace_until, only in valid and grace. |
features | array or null | Licensed feature names, if the license lists any. |
binding_mode | string or null | floating or bound. |
source | string or null | Where the token was read: db, file or env. |
reason | string or null | Why the license is invalid or missing. |
install_id | string | This installation's ID. A vendor needs it to issue a bound license. |
allows_registration | boolean | True in valid and grace. |
allows_monitoring | boolean | True in valid and grace. |
Status codes: 200; 401.
bash
curl -s https://mmune.example.com/api/v1/license/status -H "Authorization: Bearer <TOKEN>"POST /api/v1/license
Applies a new vendor-signed token for a renewal or mid-term upgrade. The signature, audience, claims schema, binding and grace window are all verified first, and a rejected token never deactivates the license in force. The applying user is recorded.
Auth: admin role.
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | The license JWT. Minimum length 1. Surrounding whitespace is trimmed. |
Response 200: {"message": "License applied", "status": {...}} where status has the same fields as GET /api/v1/license/status.
Status codes: 200; 400 with detail of {"code": "license_rejected", "reason": "<reason>"}; 401; 403; 422 empty or missing token. The reason strings begin with one of malformed_token, missing_kid, unknown_kid, signature_or_claims, claims_schema, unsupported_schema, binding_missing_install_id, binding_mismatch or already_expired.
bash
curl -s -X POST https://mmune.example.com/api/v1/license \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"token": "<LICENSE_JWT>"}'GET /api/v1/license/attestation
A signed usage report; send it to mmune unchanged.
Auth: admin role.
Response 200: attestation (string, the signed report) and instructions (string, how to hand it to mmune). Treat the report as an opaque string.
Status codes: 200; 400 with detail of {"code": "no_active_license", "message": "An attestation requires an active license."} when no active license is in force; 401; 403.
bash
curl -s https://mmune.example.com/api/v1/license/attestation -H "Authorization: Bearer <TOKEN>"Reports router: /api/v1/reports
Passive Audit reports are generated from data mmune has observed, as a background job. A finished report survives a restart.
POST /api/v1/reports/audit
Starts report generation.
Auth: Route check Permission 'write' required, plus the general write check.
Request body (JSON), all fields optional:
| Field | Type | Default | Description |
|---|---|---|---|
window_start | datetime (ISO 8601) | window_end minus 7 days | Start of the observation window. |
window_end | datetime (ISO 8601) | now, UTC | End of the observation window. |
client_name | string | null | Name printed on the report. |
Response 200: {"report_id": "<uuid>", "status": "PENDING"}.
Status codes: 200; 401; 403; 422 malformed body.
bash
curl -s -X POST https://mmune.example.com/api/v1/reports/audit \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"client_name": "Example Health", "window_start": "2026-09-01T00:00:00Z", "window_end": "2026-09-30T23:59:59Z"}'GET /api/v1/reports/status/
Reports generation status.
Auth: Route check Permission 'read' required.
Path parameter: report_id. Query parameter: cursor (integer, default 0, minimum 0) for incremental logs.
Response 200: report_id, status (PENDING, COMPLETED or FAILED; the stored status stays PENDING while the job runs), error (string or null), logs (array of strings), next_cursor (integer), created_at, updated_at.
Status codes: 200; 401; 403; 404 Report not found.
bash
curl -s "https://mmune.example.com/api/v1/reports/status/<REPORT_ID>" -H "Authorization: Bearer <TOKEN>"GET /api/v1/reports/impact-cost-config
Reads the cost assumptions the report's impact estimates use, and where each value came from.
Auth: any valid token with read.
Response 200:
| Field | Type | Description |
|---|---|---|
cost_per_record | number or null | Cost assumed per affected record. null means no dollar figure. |
cost_per_record_source | string | ui, env or default. |
cost_per_hour | number or null | Cost assumed per hour of impact. |
cost_per_hour_source | string | ui, env or default. |
Status codes: 200; 401.
bash
curl -s https://mmune.example.com/api/v1/reports/impact-cost-config -H "Authorization: Bearer <TOKEN>"PUT /api/v1/reports/impact-cost-config
Sets or clears the cost assumptions. Both fields are always written together, so an omitted or null field is stored as cleared and does not fall back to an environment value. The change persists across restarts and takes effect immediately.
Auth: Route check Permission 'write' required, plus the general write check.
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
cost_per_record | number or null | no | Minimum 0. |
cost_per_hour | number or null | no | Minimum 0. |
Response 200: same shape as the GET response, showing the values now in force.
Status codes: 200; 401; 403; 422 for a negative value.
bash
curl -s -X PUT https://mmune.example.com/api/v1/reports/impact-cost-config \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"cost_per_record": 150, "cost_per_hour": 5000}'GET /api/v1/reports/
Returns the finished report as an HTML page (content type text/html).
Auth: Route check Permission 'read' required.
Path parameter: report_id.
Status codes: 200 with the HTML body; 401; 403; 404 Report not found; 409 Report is not ready (status: <status>).
bash
curl -s https://mmune.example.com/api/v1/reports/<REPORT_ID> \
-H "Authorization: Bearer <TOKEN>" -o audit-report.htmlWorked examples
Each example assumes the base URL https://mmune.example.com and placeholder credentials. Replace them before running. The Python examples need requests; the TypeScript examples use the global fetch available in Node 18 or later and in browsers. Both stop with an error on any non-2xx response instead of continuing.
1. Log in, complete MFA if required, and refresh
Python:
python
import requests
BASE = "https://mmune.example.com"
def login(email: str, password: str, totp: str | None = None) -> dict:
r = requests.post(f"{BASE}/api/v1/auth/login", json={"email": email, "password": password})
r.raise_for_status()
body = r.json()
if body.get("mfa_required"):
if totp is None:
raise RuntimeError("Account has MFA enabled; pass a TOTP or backup code")
r = requests.post(
f"{BASE}/api/v1/auth/mfa/verify-login",
json={"mfa_token": body["mfa_token"], "token": totp},
)
r.raise_for_status()
body = r.json()
return body # access_token, token_type, expires_in, user
def refresh(current_token: str) -> dict:
# The refresh endpoint takes a still-valid access token and revokes it.
r = requests.post(f"{BASE}/api/v1/auth/refresh", json={"refresh_token": current_token})
r.raise_for_status()
return r.json() # access_token, token_type, expires_in
session = login("you@example.com", "<PASSWORD>", totp=None)
token = session["access_token"]
token = refresh(token)["access_token"] # do this before expires_in elapses
headers = {"Authorization": f"Bearer {token}"}TypeScript:
typescript
const BASE = "https://mmune.example.com";
interface LoginResult {
access_token: string;
token_type: string;
expires_in: number;
user: { id: string; email: string; name: string; role: string; permissions: string[] };
}
async function postJson<T>(path: string, body: unknown, token?: string): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path} failed: ${res.status} ${await res.text()}`);
return (await res.json()) as T;
}
async function login(email: string, password: string, totp?: string): Promise<LoginResult> {
const first = await postJson<LoginResult & { mfa_required?: boolean; mfa_token?: string }>(
"/api/v1/auth/login",
{ email, password },
);
if (!first.mfa_required) return first;
if (!totp) throw new Error("Account has MFA enabled; pass a TOTP or backup code");
return postJson<LoginResult>("/api/v1/auth/mfa/verify-login", {
mfa_token: first.mfa_token,
token: totp,
});
}
async function refresh(currentToken: string): Promise<string> {
const r = await postJson<{ access_token: string }>("/api/v1/auth/refresh", {
refresh_token: currentToken,
});
return r.access_token;
}2. License status, renewal and attestation
The renewal and attestation calls need an admin token. In Python, reuse BASE and headers from example 1.
python
import requests
status = requests.get(f"{BASE}/api/v1/license/status", headers=headers)
status.raise_for_status()
s = status.json()
print(s["state"], f'{s["systems_used"]}/{s["max_systems"]}', s["expires_at"], s["install_id"])
# Apply a renewal token received from the vendor.
renewal = requests.post(
f"{BASE}/api/v1/license",
headers=headers,
json={"token": "<LICENSE_JWT>"},
)
if renewal.status_code == 400:
raise RuntimeError(renewal.json()["detail"]["reason"]) # license_rejected
renewal.raise_for_status()
print(renewal.json()["status"]["state"])
# Signed usage report to send back to the vendor.
att = requests.get(f"{BASE}/api/v1/license/attestation", headers=headers)
att.raise_for_status()
attestation = att.json()["attestation"]typescript
async function getJson<T>(path: string, token: string): Promise<T> {
const res = await fetch(`${BASE}${path}`, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error(`${path} failed: ${res.status} ${await res.text()}`);
return (await res.json()) as T;
}
interface LicenseStatus {
state: "valid" | "grace" | "expired" | "missing" | "invalid";
max_systems: number;
systems_used: number;
expires_at: string | null;
install_id: string;
allows_registration: boolean;
allows_monitoring: boolean;
}
async function licenseFlow(adminToken: string, renewalJwt: string): Promise<string> {
const status = await getJson<LicenseStatus>("/api/v1/license/status", adminToken);
console.log(status.state, status.systems_used, status.max_systems);
const applied = await postJson<{ message: string; status: LicenseStatus }>(
"/api/v1/license",
{ token: renewalJwt },
adminToken,
);
console.log(applied.status.state);
const att = await getJson<{ attestation: string; instructions: string }>(
"/api/v1/license/attestation",
adminToken,
);
return att.attestation;
}3. Generate the Passive Audit report and read compliance scores
Generation is asynchronous: start it, poll the status until it leaves PENDING, then download the HTML. The scores come from GET /api/v1/compliance/posture.
python
import time
import requests
start = requests.post(
f"{BASE}/api/v1/reports/audit",
headers=headers,
json={"client_name": "Example Health"}, # window defaults to the last 7 days
)
start.raise_for_status()
report_id = start.json()["report_id"]
while True:
st = requests.get(f"{BASE}/api/v1/reports/status/{report_id}", headers=headers)
st.raise_for_status()
info = st.json()
if info["status"] == "COMPLETED":
break
if info["status"] == "FAILED":
raise RuntimeError(info["error"])
time.sleep(3)
html = requests.get(f"{BASE}/api/v1/reports/{report_id}", headers=headers)
html.raise_for_status()
open("audit-report.html", "w", encoding="utf-8").write(html.text)
posture = requests.get(f"{BASE}/api/v1/compliance/posture", headers=headers)
posture.raise_for_status()
for name, fw in posture.json()["frameworks"].items():
print(name, fw["status"], f'{fw["controls_passed"]}/{fw["controls_total"]}')typescript
async function auditReportAndScores(token: string): Promise<void> {
const { report_id } = await postJson<{ report_id: string; status: string }>(
"/api/v1/reports/audit",
{ client_name: "Example Health" },
token,
);
for (;;) {
const info = await getJson<{ status: string; error: string | null }>(
`/api/v1/reports/status/${report_id}`,
token,
);
if (info.status === "COMPLETED") break;
if (info.status === "FAILED") throw new Error(info.error ?? "report failed");
await new Promise((r) => setTimeout(r, 3000));
}
const htmlRes = await fetch(`${BASE}/api/v1/reports/${report_id}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!htmlRes.ok) throw new Error(`report download failed: ${htmlRes.status}`);
const html = await htmlRes.text(); // save or serve as needed
const posture = await getJson<{
frameworks: Record<string, { status: string; controls_passed: number; controls_total: number }>;
}>("/api/v1/compliance/posture", token);
for (const [name, fw] of Object.entries(posture.frameworks)) {
console.log(name, fw.status, `${fw.controls_passed}/${fw.controls_total}`);
}
}4. Query the audit log
Admin token required. This pulls failed logins since a given time, walking pages until one comes back short.
python
import requests
params = {
"event_type": "user_login_failed",
"start_time": "2026-10-01T00:00:00Z",
"page_size": 100,
"page": 1,
}
events = []
while True:
r = requests.get(f"{BASE}/api/v1/audit/events", headers=headers, params=params)
r.raise_for_status()
batch = r.json()["events"]
events.extend(batch)
if len(batch) < params["page_size"]:
break
params["page"] += 1
for e in events:
print(e["timestamp"], e["username"], e["ip_address"], e["status"])typescript
interface AuditEvent {
event_id: string;
event_type: string;
severity: string;
timestamp: string;
username: string | null;
ip_address: string | null;
status: string;
}
async function failedLogins(adminToken: string, since: string): Promise<AuditEvent[]> {
const pageSize = 100;
const all: AuditEvent[] = [];
for (let page = 1; ; page++) {
const qs = new URLSearchParams({
event_type: "user_login_failed",
start_time: since,
page: String(page),
page_size: String(pageSize),
});
const { events } = await getJson<{ events: AuditEvent[] }>(
`/api/v1/audit/events?${qs}`,
adminToken,
);
all.push(...events);
if (events.length < pageSize) return all;
}
}5. Run a sensitive-data check and mask rows
Start a Guardian scan (admin token), wait for it, read the findings, build a masking policy from the scan, then mask rows.
python
import time
import requests
scan = requests.post(f"{BASE}/api/v1/guardian/scan", headers=headers, json={"target": "all"})
scan.raise_for_status()
job_id = scan.json()["job_id"]
cursor = 0
while True:
r = requests.get(
f"{BASE}/api/v1/guardian/status/{job_id}", headers=headers, params={"cursor": cursor}
)
r.raise_for_status()
job = r.json()
cursor = job["next_cursor"]
if job["status"] == "COMPLETED":
break
if job["status"] == "FAILED":
raise RuntimeError(job["error"])
time.sleep(3)
print(job["results"]["summary"]) # total_fields, flagged_fields, risk_score
findings = requests.get(f"{BASE}/api/v1/compliance/findings", headers=headers)
findings.raise_for_status()
print(findings.json()["summary"]) # high, medium, low, total
policy = requests.post(
f"{BASE}/api/v1/masking/generate", headers=headers, json={"scan_job_id": job_id}
)
policy.raise_for_status()
policy_id = policy.json()["policy_id"]
masked = requests.post(
f"{BASE}/api/v1/masking/apply",
headers=headers,
json={"policy_id": policy_id, "rows": [{"ssn": "123-45-6789", "email": "a@example.com"}]},
)
masked.raise_for_status()
print(masked.json()["rows"])typescript
async function scanAndMask(adminToken: string): Promise<Record<string, unknown>[]> {
const { job_id } = await postJson<{ job_id: string; status: string }>(
"/api/v1/guardian/scan",
{ target: "all" },
adminToken,
);
let cursor = 0;
for (;;) {
const job = await getJson<{ status: string; next_cursor: number; error: string | null }>(
`/api/v1/guardian/status/${job_id}?cursor=${cursor}`,
adminToken,
);
cursor = job.next_cursor;
if (job.status === "COMPLETED") break;
if (job.status === "FAILED") throw new Error(job.error ?? "scan failed");
await new Promise((r) => setTimeout(r, 3000));
}
const findings = await getJson<{ summary: { high: number; medium: number; low: number; total: number } }>(
"/api/v1/compliance/findings",
adminToken,
);
console.log(findings.summary);
const { policy_id } = await postJson<{ policy_id: string }>(
"/api/v1/masking/generate",
{ scan_job_id: job_id },
adminToken,
);
const masked = await postJson<{ rows: Record<string, unknown>[] }>(
"/api/v1/masking/apply",
{ policy_id, rows: [{ ssn: "123-45-6789", email: "a@example.com" }] },
adminToken,
);
return masked.rows;
}