On this page

REST API

Everything the app can do is available over HTTP. This chapter is the contract: the endpoint index below is complete, and /openapi.json owns the request and response shapes.

AspectDetail
Base path/api/v1
AuthAuthorization: Bearer <access token> — session JWT or personal access token
Schema/openapi.json, browsable at /docs (Swagger) and /redoc
HealthGET/health, outside the version prefix
ContentJSON, except multipart uploads and stream responses

Conventions#

ConventionBehaviour
Long operationsAnswer immediately with { "job_id": … }; poll GET/jobs/{id} or stream GET/jobs/{id}/stream.
Streamingtext/event-stream for job progress, platform events, and AI chat.
ErrorsJSON with a detail field — a string for HTTP errors, an array of objects for 422 validation failures.
OwnershipAnother user's resource is indistinguishable from a missing one (404).
IdempotencyGET, PUT, DELETE are safe to retry; POST creates.
Paginationlimit/offset on the row-level list endpoints (events, variants, activities, edits, OCEL views, admin listings); top-level collections return full lists and GET/jobs takes limit only. Cursor pagination is MCP-only.
Soft deletesDeleting a log, folder, or watched folder marks it deleted and removes the data; deleting a team is a hard delete that cascades to its memberships.

Endpoint groups#

Every route the API serves, one endpoint per row. Paths are relative to /api/v1. The Request and Response cells show the shape of a call — a query string as it would be sent, and a formatted JSON body under its status code. A … marks elided fields, further array items, or a nested object left unexpanded; /openapi.json stays authoritative for the complete schema.

The table is generated from that schema: after changing a route, a request model or a response status, run uv run python landing/docs/gen-endpoints.py and rebuild.

GroupEndpointRequestResponse
ImportPOST/event-logs/stage{"file":"…"}200
{
  "kind": "…",
  "staging_token": "…",
  "source_format": "…",
  "log_model": "case_centric", …
}
ImportPOST/event-logs/stage/{token}/extract{"member":"…"}200
{
  "kind": "…",
  "staging_token": "…",
  "source_format": "…",
  "log_model": "case_centric", …
}
ImportDELETE/event-logs/stage/{token}—204 —
ImportPOST/event-logs
{
  "file": "…",
  "staging_token": "…",
  "name": "nightly import",
  "csv_mapping": "…", …
}
202
{
  "log_id": "018f2c9a…",
  "job_id": "018f2c9a…"
}
ImportPOST/event-logs/from-url
{
  "url": "https://example.com/log.xes",
  "name": "nightly import",
  "csv_mapping": "…", …
}
202
{
  "log_id": "018f2c9a…",
  "job_id": "018f2c9a…"
}
ImportPOST/event-logs/probe-xml{"file":"…"}200
{
  "format_hint": "generic",
  "event_element": "…",
  "events_sampled": 0,
  "fields": [
    {…}
  ], …
}
ImportPOST/event-logs/probe-json{"file":"…"}200
{
  "format_hint": "generic",
  "event_path": "…",
  "events_sampled": 0,
  "fields": [
    {…}
  ], …
}
LogsGET/event-logs?q=…&status=ready200
[
  {
    "id": "018f2c9a…",
    "name": "nightly import",
    "status": "importing",
    "source_format": "…", …
  }
]
LogsGET/event-logs/{log_id}—200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "status": "importing",
  "source_format": "…", …
}
LogsPATCH/event-logs/{log_id}
{
  "name": "nightly import",
  "description": "…",
  "column_overrides": null, …
}
200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "status": "importing",
  "source_format": "…", …
}
LogsDELETE/event-logs/{log_id}—204 —
Log dataPUT/event-logs/{log_id}/active-filter
{
  "filter": [
    {
      "field": "…",
      "op": "…",
      "value": null
    }
  ]
}
200
{
  "active_filter": [
    {
      "field": "…",
      "op": "…",
      "value": null
    }
  ],
  "modules_retriggered": false
}
Log dataGET/event-logs/{log_id}/activities—200
{
  "rows": [
    {
      "activity": "…",
      "count": 25
    }
  ],
  "total": 25
}
Log dataGET/event-logs/{log_id}/activities/cases?limit=100&offset=0&name=nightly import200
{
  "rows": [
    {
      "case_id": "018f2c9a…", …
    }
  ],
  "total": 25,
  "offset": 25,
  "limit": 25
}
Log dataGET/event-logs/{log_id}/activities/detail?name=nightly import200
{
  "activity": "…",
  "event_count": 25,
  "event_pct": 0.5,
  "case_count": 25,
  "case_pct": 0.5, …
}
Log dataGET/event-logs/{log_id}/columns/{field}/values?limit=500&q=…200
{
  "field": "…",
  "values": [
    {
      "value": "…",
      "count": 25
    }
  ],
  "total_distinct": 25,
  "truncated": false
}
Log dataGET/event-logs/{log_id}/data-quality—200
{
  "total_events": 25,
  "columns": [
    {
      "column": "…",
      "label": "nightly import", …
    }
  ]
}
Log dataPOST/event-logs/{log_id}/duplicate—201
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "status": "importing",
  "source_format": "…", …
}
Log dataGET/event-logs/{log_id}/edits?limit=50&offset=0200
{
  "rows": [
    {
      "id": 25,
      "log_id": "018f2c9a…",
      "row_index": 25,
      "field": "…", …
    }
  ],
  "total": 25, …
}
Log dataGET/event-logs/{log_id}/events?limit=50&offset=0&q=…200
{
  "rows": [
    "…"
  ],
  "total": 25,
  "offset": 25,
  "limit": 25,
  "columns": [
    {…}
  ],
  "header": {…}
}
Log dataPOST/event-logs/{log_id}/events/bulk-fill
{
  "row_indices": [
    25
  ],
  "field": "…",
  "value": null
}
200
{
  "updated": 25,
  "header": {
    "events_count": 25,
    "cases_count": 25, …
  }
}
Log dataPATCH/event-logs/{log_id}/events/{row_index}
{
  "field": "…",
  "value": null
}
200
{
  "row": null,
  "row_index": 25,
  "new_row_index": 25,
  "header": {…}
}
Log dataGET/event-logs/{log_id}/module-runs—200
[
  {
    "module_id": "018f2c9a…",
    "status": "idle",
    "job_id": "018f2c9a…",
    "precompute": "eager", …
  }
]
Log dataPOST/event-logs/{log_id}/module-runs/{module_id}{"force":false}200
[
  {
    "module_id": "018f2c9a…",
    "status": "idle",
    "job_id": "018f2c9a…",
    "precompute": "eager", …
  }
]
Log dataPOST/event-logs/{log_id}/reimport—202
{
  "log_id": "018f2c9a…",
  "job_id": "018f2c9a…"
}
Log dataPOST/event-logs/{log_id}/remap
{
  "case_id": "018f2c9a…",
  "activity": "…",
  "timestamp": "2026-01-01T00:00:00Z", …
}
202
{
  "log_id": "018f2c9a…",
  "job_id": "018f2c9a…"
}
Log dataGET/event-logs/{log_id}/time-bounds—200
{
  "field": "…",
  "min_ts": "…",
  "max_ts": "…"
}
Log dataGET/event-logs/{log_id}/variants?limit=50&offset=0200
{
  "rows": [
    {
      "rank": 25,
      "variant_id": "018f2c9a…",
      "activities": [
        "…"
      ], …
    }
  ],
  "total": 25, …
}
Log dataGET/event-logs/{log_id}/variants/{variant_id}—200
{
  "rank": 25,
  "variant_id": "018f2c9a…",
  "activities": [
    "…"
  ],
  "case_count": 25,
  "case_pct": 0.5, …
}
Log dataGET/event-logs/{log_id}/variants/{variant_id}/cases?limit=100&offset=0200
{
  "rows": [
    {
      "case_id": "018f2c9a…", …
    }
  ],
  "total": 25,
  "offset": 25,
  "limit": 25
}
OCELGET/event-logs/{log_id}/ocel/events?limit=100&offset=0&q=…200
{
  "rows": [
    "…"
  ],
  "columns": [
    "…"
  ],
  "total": 25,
  "offset": 25,
  "limit": 25
}
OCELGET/event-logs/{log_id}/ocel/object-types—200
[
  {
    "type": "…",
    "count": 25
  }
]
OCELGET/event-logs/{log_id}/ocel/objects?limit=100&offset=0&q=…200
{
  "rows": [
    "…"
  ],
  "columns": [
    "…"
  ],
  "total": 25,
  "offset": 25,
  "limit": 25
}
OCELGET/event-logs/{log_id}/ocel/overview—200
{
  "events_count": 25,
  "objects_count": 25,
  "object_types_count": 25,
  "relations_count": 25, …
}
OCELGET/event-logs/{log_id}/ocel/relationships?limit=100&offset=0200
{
  "rows": [
    "…"
  ],
  "columns": [
    "…"
  ],
  "total": 25,
  "offset": 25,
  "limit": 25
}
OrganisationGET/folders—200
[
  {
    "id": "018f2c9a…",
    "name": "nightly import",
    "parent_id": "018f2c9a…",
    "position": 0, …
  }
]
OrganisationPOST/folders
{
  "name": "nightly import",
  "parent_id": "018f2c9a…"
}
201
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "parent_id": "018f2c9a…",
  "position": 0, …
}
OrganisationPOST/folders/reorder
{
  "items": [
    {
      "kind": "folder",
      "id": "018f2c9a…", …
    }
  ]
}
204 —
OrganisationPATCH/folders/{folder_id}
{
  "name": "nightly import",
  "parent_id": "018f2c9a…",
  "position": 25
}
200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "parent_id": "018f2c9a…",
  "position": 0, …
}
OrganisationDELETE/folders/{folder_id}—204 —
OrganisationGET/watched-folders—200
[
  {
    "id": "018f2c9a…",
    "name": "nightly import",
    "source_path": "…",
    "mode": "manual", …
  }
]
OrganisationPOST/watched-folders
{
  "name": "nightly import",
  "source_path": "…",
  "mode": "manual",
  "interval_seconds": 25, …
}
201
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "source_path": "…",
  "mode": "manual", …
}
OrganisationGET/watched-folders/{watch_id}—200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "source_path": "…",
  "mode": "manual", …
}
OrganisationPATCH/watched-folders/{watch_id}
{
  "name": "nightly import",
  "mode": "manual",
  "interval_seconds": 25,
  "status": "active", …
}
200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "source_path": "…",
  "mode": "manual", …
}
OrganisationDELETE/watched-folders/{watch_id}—204 —
OrganisationPOST/watched-folders/{watch_id}/scan—200
{
  "found": 0,
  "imported": 0,
  "skipped": 0,
  "failed": 0
}
JobsGET/jobs?limit=100&status=ready200
[
  {
    "id": "018f2c9a…",
    "type": "…",
    "title": "nightly import",
    "subtitle": "…", …
  }
]
JobsPOST/jobs/cancel-all—200 null
JobsPOST/jobs/queue/pause—204 —
JobsPOST/jobs/queue/resume—204 —
JobsGET/jobs/{job_id}—200
{
  "id": "018f2c9a…",
  "type": "…",
  "title": "nightly import",
  "subtitle": "…", …
}
JobsPOST/jobs/{job_id}/cancel—204 —
JobsPOST/jobs/{job_id}/retry—200 null
JobsGET/jobs/{job_id}/stream—200 null
EventsGET/events?topic=["job.progress"]200 null
ModulesPOST/modules/install{"file":"…"}202 {"job_id":"018f2c9a…"}
ModulesPOST/modules/restore-defaults—200 {"restored":["…"]}
ModulesGET/modules—200
[
  {
    "id": "018f2c9a…",
    "name": "nightly import",
    "version": "…",
    "category": "…", …
  }
]
ModulesGET/modules/cards—200
[
  {
    "module_id": "018f2c9a…",
    "module_name": "…",
    "widget_id": "018f2c9a…", …
  }
]
ModulesGET/modules/readme—200 null
ModulesDELETE/modules/{module_id}—204 —
ModulesGET/modules/{module_id}/assets/{asset_path}—200 null
ModulesGET/modules/{module_id}/config—200
{
  "config": null,
  "enabled": true,
  "controlled_by_admin": false,
  "controlled_cards": null
}
ModulesPUT/modules/{module_id}/config
{
  "config": null,
  "enabled": true,
  "controlled_by_admin": false,
  "controlled_cards": null
}
200
{
  "config": null,
  "enabled": true,
  "controlled_by_admin": false,
  "controlled_cards": null
}
ModulesGET/modules/{module_id}/config-schema—200 null
ModulesGET/modules/{module_id}/layout?log_id=018f2c9a…200 {"layout":null}
ModulesPUT/modules/{module_id}/layout?log_id=018f2c9a… · {"layout":null}200 {"layout":null}
ModulesGET/modules/{module_id}/manifest—200 null
DatasetsGET/datasets/catalog—200
[
  {
    "module_id": "018f2c9a…",
    "module_name": "…",
    "dataset_id": "018f2c9a…", …
  }
]
DatasetsGET/datasets/{module_id}/{dataset_id}?log_id=018f2c9a…200
{
  "shape": "table",
  "schema": {
    "columns": [
      {…}
    ]
  },
  "data": {
    "columns": [
      {…}
    ], …
  },
  "meta": {…}
}
DatasetsPOST/datasets/{module_id}/{dataset_id}/transform
{
  "log_id": "018f2c9a…",
  "transforms": [
    "…"
  ]
}
200
{
  "shape": "table",
  "schema": {
    "columns": [
      {…}
    ]
  },
  "data": {
    "columns": [
      {…}
    ], …
  },
  "meta": {…}
}
DashboardsPOST/dashboards/import
{
  "name": "nightly import",
  "description": "…",
  "log_model": "case_centric",
  "items": [
    {…}
  ], …
}
201
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "description": "…",
  "event_log_id": "018f2c9a…", …
}
DashboardsGET/dashboards—200
[
  {
    "id": "018f2c9a…",
    "name": "nightly import",
    "description": "…", …
  }
]
DashboardsPOST/dashboards
{
  "name": "nightly import",
  "description": "…",
  "event_log_id": "018f2c9a…", …
}
201
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "description": "…",
  "event_log_id": "018f2c9a…", …
}
DashboardsGET/dashboards/templates—200
[
  {
    "id": "018f2c9a…",
    "name": "nightly import",
    "description": "…", …
  }
]
DashboardsGET/dashboards/{dashboard_id}—200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "description": "…",
  "event_log_id": "018f2c9a…", …
}
DashboardsPATCH/dashboards/{dashboard_id}
{
  "name": "nightly import",
  "description": "…",
  "event_log_id": "018f2c9a…",
  "items": [
    {…}
  ], …
}
200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "description": "…",
  "event_log_id": "018f2c9a…", …
}
DashboardsDELETE/dashboards/{dashboard_id}—204 —
DashboardsGET/dashboards/{dashboard_id}/export—200
{
  "kind": "…",
  "version": 1,
  "name": "nightly import",
  "description": "…", …
}
DashboardsGET/dashboards/{dashboard_id}/shares—200
[
  {
    "id": "018f2c9a…",
    "dashboard_id": "018f2c9a…",
    "kind": "user",
    "target_id": "018f2c9a…", …
  }
]
DashboardsPOST/dashboards/{dashboard_id}/shares
{
  "target_user_id": "018f2c9a…",
  "target_team_id": "018f2c9a…"
}
201
{
  "id": "018f2c9a…",
  "dashboard_id": "018f2c9a…",
  "kind": "user",
  "target_id": "018f2c9a…", …
}
DashboardsDELETE/dashboards/{dashboard_id}/shares/{share_id}—204 —
SharingGET/sharing/shared-with-me—200
[
  {
    "id": "018f2c9a…",
    "name": "nightly import",
    "description": "…", …
  }
]
SharingGET/sharing/targets—200
[
  {
    "kind": "user",
    "id": "018f2c9a…",
    "label": "nightly import",
    "sublabel": "…"
  }
]
AIPOST/ai/chat
{
  "messages": [
    {
      "role": "user",
      "content": "…"
    }
  ],
  "context": {…},
  "nav_hint": {…}
}
200 null
AIGET/ai/config—200
{
  "system_prompt": "…",
  "anthropic_base_url": "https://api.openai.com/v1", …
}
AIPUT/ai/config
{
  "system_prompt": "…",
  "anthropic": {
    "api_key": "…", …
  },
  "openai": {…},
  "unigpt": {…},
  "custom": {…}, …
}
200
{
  "system_prompt": "…",
  "anthropic_base_url": "https://api.openai.com/v1", …
}
AIPOST/ai/guidance/import/column-mapping
{
  "headers": [
    "…"
  ],
  "sample_rows": [
    [
      "…"
    ]
  ]
}
200 {"suggestions":null}
AIPOST/ai/guidance/import/quality/{log_id}{"force":false}200
{
  "cached": false,
  "output_hash": "…",
  "generated_at": 1767225600,
  "model": "gpt-4o-mini", …
}
AIPOST/ai/guidance/module/{module_id}?log_id=018f2c9a… · {"force":false}200
{
  "cached": false,
  "output_hash": "…",
  "generated_at": 1767225600,
  "model": "gpt-4o-mini", …
}
AIDELETE/ai/guidance/module/{module_id}?log_id=018f2c9a…200 null
AIPOST/ai/guidance/module/{module_id}/stream?log_id=018f2c9a…200 null
AIPOST/ai/guidance/process/{log_id}{"force":false}200
{
  "cached": false,
  "output_hash": "…",
  "generated_at": 1767225600,
  "model": "gpt-4o-mini", …
}
AIPOST/ai/models/{provider}—200
{
  "models": [
    {
      "id": "018f2c9a…",
      "display_name": "…",
      "created": 25
    }
  ]
}
AIGET/ai/pricing—200 null
AIPOST/ai/route
{
  "message": "…",
  "context": {
    "log_id": "018f2c9a…", …
  }
}
200
{
  "intent": "chat",
  "confidence": 0.5,
  "targets": [
    {
      "id": "018f2c9a…", …
    }
  ],
  "actions": [
    {…}
  ]
}
AccountGET/api-tokens—200
[
  {
    "id": "018f2c9a…",
    "name": "nightly import",
    "token_prefix": "…",
    "scopes": [
      "…"
    ], …
  }
]
AccountPOST/api-tokens
{
  "name": "nightly import",
  "scopes": [
    "…"
  ],
  "expires_in_days": 25
}
200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "token_prefix": "…",
  "scopes": [
    "…"
  ], …
}
AccountGET/api-tokens/consent—200
{
  "required": false,
  "consented": false
}
AccountPUT/api-tokens/consent{"consented":false}200
{
  "required": false,
  "consented": false
}
AccountGET/api-tokens/mcp-info—200
{
  "enabled": false,
  "url": "https://example.com/log.xes",
  "require_consent": false, …
}
AccountDELETE/api-tokens/{token_id}—200 null
AccountGET/onboarding—200
{
  "completed": false,
  "experience_level": "beginner",
  "tour_completed": false
}
AccountPUT/onboarding
{
  "completed": false,
  "experience_level": "beginner",
  "tour_completed": false
}
200
{
  "completed": false,
  "experience_level": "beginner",
  "tour_completed": false
}
AccountGET/preferences/{key}—200 null
AccountPUT/preferences/{key}—200 null
AccountGET/usage/config—200
{
  "enabled": false,
  "retention_days": 25,
  "capture_clicks": true,
  "capture_perf": true, …
}
AccountPUT/usage/config
{
  "enabled": false,
  "retention_days": 25,
  "capture_clicks": true,
  "capture_perf": true, …
}
200
{
  "enabled": false,
  "retention_days": 25,
  "capture_clicks": true,
  "capture_perf": true, …
}
AccountGET/usage/export?format=ndjson200 null
AccountGET/usage/summary—200
{
  "enabled": false,
  "total_events": 25,
  "total_sessions": 25,
  "sessions_last_30d": 25, …
}
AccountPOST/usage/sync—202 null
AccountDELETE/usage/sync—200
{
  "deleted_events": 25,
  "deleted_sessions": 25,
  "new_anon_user_id_seed": "…"
}
SystemGET/system/diagnostics—200
{
  "platform_version": "…",
  "python": "…",
  "is_admin": false,
  "system": {…},
  "versions": {…}, …
}
SystemGET/system/jobs—200
{
  "worker_concurrency": 25,
  "min": 1,
  "max": 8,
  "is_admin": false
}
SystemPUT/system/jobs{"worker_concurrency":25}200
{
  "worker_concurrency": 25,
  "min": 1,
  "max": 8,
  "is_admin": false
}
SystemGET/system/mcp—200
{
  "boot_enabled": false,
  "enabled": false,
  "mint_policy": "…",
  "boot_read_only": false, …
}
SystemPUT/system/mcp
{
  "enabled": false,
  "mint_policy": "all_users",
  "read_only": false
}
200
{
  "boot_enabled": false,
  "enabled": false,
  "mint_policy": "…",
  "boot_read_only": false, …
}
SystemGET/system/mcp-metrics—200 null
SystemGET/system/resources—200
{
  "cpu": {
    "current_pct": 0.5,
    "max_pct": 0.5,
    "cores_logical": 25, …
  },
  "memory": {…}, …
}
SystemGET/system/storage—200 null
AdminGET/admin/api-tokens—200
[
  {
    "id": "018f2c9a…",
    "user_id": "018f2c9a…",
    "user_email": "ada@example.com", …
  }
]
AdminDELETE/admin/api-tokens/{token_id}—200 null
AdminGET/admin/controls/ai/config—200
{
  "system_prompt": "…",
  "anthropic_base_url": "https://api.openai.com/v1", …
}
AdminPUT/admin/controls/ai/config
{
  "system_prompt": "…",
  "anthropic": {
    "api_key": "…", …
  },
  "openai": {…},
  "unigpt": {…},
  "custom": {…}, …
}
200
{
  "system_prompt": "…",
  "anthropic_base_url": "https://api.openai.com/v1", …
}
AdminPOST/admin/controls/ai/models/{provider}—200
{
  "models": [
    {
      "id": "018f2c9a…",
      "display_name": "…",
      "created": 25
    }
  ]
}
AdminGET/admin/controls/items—200
{
  "items": [
    {
      "scope": "setting",
      "key": "…",
      "label": "nightly import", …
    }
  ]
}
AdminPUT/admin/controls/items/{scope}/{key}
{
  "control_mode": "user",
  "admin_value": null
}
200
{
  "scope": "setting",
  "key": "…",
  "label": "nightly import",
  "description": "…", …
}
AdminGET/admin/dashboard-shares—200
[
  {
    "id": "018f2c9a…",
    "dashboard_id": "018f2c9a…",
    "dashboard_name": "…",
    "owner_label": "…", …
  }
]
AdminDELETE/admin/dashboard-shares/{share_id}—204 —
AdminGET/admin/export-info—200
{
  "is_admin": false,
  "user_count": 25,
  "event_count": 25,
  "db_size_bytes": 25
}
AdminGET/admin/export/event-log.xes—200 null
AdminGET/admin/export/events-ocel2—200 null
AdminGET/admin/export/events.csv—200 null
AdminGET/admin/export/events.ndjson—200 null
AdminGET/admin/export/facets—200
{
  "users": [
    {
      "id": "018f2c9a…",
      "email": "ada@example.com", …
    }
  ],
  "event_types": [
    "…"
  ], …
}
AdminGET/admin/export/metadata-db—200 null
AdminGET/admin/export/preview—200
{
  "matched_events": 25,
  "matched_sessions": 25,
  "distinct_users": 25, …
}
AdminGET/admin/insights/event-logs?limit=50&offset=0&q=…&status=ready200
{
  "total": 25,
  "items": [
    {
      "id": "018f2c9a…",
      "name": "nightly import", …
    }
  ]
}
AdminGET/admin/insights/event-logs/{log_id}/download—200 null
AdminGET/admin/insights/jobs—200
{
  "days": 25,
  "runtime": {
    "concurrency": 25,
    "live_workers": 25,
    "queue_depth": 25, …
  }, …
}
AdminGET/admin/insights/overview—200
{
  "days": 25,
  "kpis": {
    "user_count": 25,
    "log_count": 25,
    "events_ingested": 25, …
  }, …
}
AdminGET/admin/insights/storage—200
{
  "backend_mode": "…",
  "s3_used_bytes": 25,
  "s3_object_count": 25,
  "s3_quota_bytes": 25, …
}
AdminGET/admin/insights/usage—200
{
  "days": 25,
  "installs_by_module": [
    {
      "label": "nightly import",
      "count": 25
    }
  ],
  "modules": [
    {…}
  ], …
}
AdminGET/admin/insights/users—200
{
  "days": 25,
  "user_count": 25,
  "active_users_in_range": 25,
  "onboarding_completed": 25, …
}
AdminGET/admin/jobs?limit=100&offset=0&q=…&status=ready200
{
  "total": 25,
  "items": [
    {
      "id": "018f2c9a…",
      "type": "…", …
    }
  ],
  "summary": {…}
}
AdminPOST/admin/jobs/cancel-all{"user_id":"018f2c9a…"}200 null
AdminPOST/admin/jobs/queue/pause{"user_id":"018f2c9a…"}204 —
AdminPOST/admin/jobs/queue/resume{"user_id":"018f2c9a…"}204 —
AdminPOST/admin/jobs/{job_id}/cancel—204 —
AdminPOST/admin/jobs/{job_id}/kill—204 —
AdminGET/admin/jobs/{job_id}/logs?limit=500200
{
  "job_id": "018f2c9a…",
  "lines": [
    {
      "ts": 0.5,
      "level": "…",
      "event": "…", …
    }
  ],
  "truncated": false
}
AdminPOST/admin/jobs/{job_id}/retry—200 null
AdminGET/admin/modules—200
[
  {
    "id": "018f2c9a…",
    "name": "nightly import",
    "version": "…",
    "category": "…", …
  }
]
AdminPUT/admin/modules/{module_id}/default{"is_default":false}200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "version": "…",
  "category": "…", …
}
AdminPOST/admin/modules/{module_id}/installs{"user_id":"018f2c9a…"}200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "version": "…",
  "category": "…", …
}
AdminDELETE/admin/modules/{module_id}/installs/{user_id}—204 —
AdminPUT/admin/modules/{module_id}/precompute{"mode":"eager"}200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "version": "…",
  "category": "…", …
}
AdminPUT/admin/modules/{module_id}/withhold{"withheld":false}200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "version": "…",
  "category": "…", …
}
AdminGET/admin/teams—200
[
  {
    "id": "018f2c9a…",
    "name": "nightly import",
    "member_count": 0, …
  }
]
AdminPOST/admin/teams
{
  "name": "nightly import"
}
201
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "member_count": 0, …
}
AdminPATCH/admin/teams/{team_id}
{
  "name": "nightly import"
}
200
{
  "id": "018f2c9a…",
  "name": "nightly import",
  "member_count": 0, …
}
AdminDELETE/admin/teams/{team_id}—204 —
AdminGET/admin/teams/{team_id}/members—200
[
  {
    "user_id": "018f2c9a…",
    "role": "…",
    "email": "ada@example.com", …
  }
]
AdminPOST/admin/teams/{team_id}/members
{
  "user_id": "018f2c9a…",
  "role": "owner"
}
201
{
  "user_id": "018f2c9a…",
  "role": "…",
  "email": "ada@example.com",
  "preferred_username": "…", …
}
AdminDELETE/admin/teams/{team_id}/members/{user_id}—204 —
AdminGET/admin/users—200
[
  {
    "id": "018f2c9a…",
    "email": "ada@example.com",
    "preferred_username": "…", …
  }
]
AdminGET/admin/users/{user_id}—200
{
  "id": "018f2c9a…",
  "email": "ada@example.com",
  "preferred_username": "…", …
}
AdminDELETE/admin/users/{user_id}—200
{
  "deleted": false,
  "jobs_cancelled": 25,
  "modules_torn_down": 25,
  "keycloak_deleted": false, …
}
MetaGET/health—200
{
  "status": "ready",
  "version": "…"
}

Admin routes are gated by require_admin server-side, and a personal access token can never carry the admin scope. A module's own @route handlers are mounted at /api/v1/modules/{module_id}/* by the loader, so they appear in /openapi.json rather than here.

Type generation#

Terminal
make codegen          # API must be listening on :8000

Regenerates apps/web/lib/api-types.ts from /openapi.json. The web app never hand-writes an API type, so a route change is a breaking change for the frontend until codegen runs.

Worked example: automate an import#

nightly_import.py
import os
import time

import httpx

client = httpx.Client(
    base_url=os.environ["MATE_API_URL"].rstrip("/") + "/api/v1",
    headers={"Authorization": f"Bearer {os.environ['MATE_TOKEN']}"},  # mate_pat_…
    timeout=60.0,
)


def wait_for_job(job_id: str) -> dict:
    while True:
        job = client.get(f"/jobs/{job_id}").raise_for_status().json()
        if job["status"] in {"completed", "failed", "cancelled"}:
            return job
        time.sleep(3)


# 1. import — answers with a job id, never blocks
created = client.post(
    "/event-logs/from-url", json={"url": os.environ["MATE_SOURCE_URL"], "name": "nightly"}
).raise_for_status().json()
log_id = created["log_id"]
job = wait_for_job(created["job_id"])
assert job["status"] == "completed", job.get("error")

# 2. the log is ready once it is parsed; modules compute on demand, so ask for the one you need
while client.get(f"/event-logs/{log_id}").json()["status"] != "ready":
    time.sleep(2)
while True:
    states = client.post(f"/event-logs/{log_id}/module-runs/performance_java").raise_for_status().json()
    if all(s["status"] == "ready" for s in states):  # [] when the module has nothing to precompute
        break
    if any(s["status"] in ("failed", "cancelled", "unavailable") for s in states):
        raise RuntimeError(states)
    time.sleep(2)

# 3. read a dataset a module publishes — rows live under `data` in the envelope
rows = client.get(
    "/datasets/performance_java/activity-performance", params={"log_id": log_id}
).json()["data"]["rows"]
print(rows)

Rules that keep such a script correct: retry transport errors and 5xx, never 4xx; poll instead of a tight loop; ask POST/event-logs/{id}/module-runs/{module} (idempotent) before reading a module's results, because they only exist once its run is ready; the dataset endpoint takes only log_id (a dataset's declared parameters are set on its card, not as query parameters); and remember that importing the same file twice creates two logs — de-duplicate on your side.