Appearance
Integrations and discovery API
What this page covers
This page is the reference for the mmune endpoints that find systems in an estate, configure and register them, scan their schemas, manage discovery agents and migration projects, and accept probe heartbeats. It covers these groups of endpoints, with the path prefix each one uses:
| Group | Path prefix |
|---|---|
| Integrations | /api/v1/integrations |
| Integration OAuth | /api/v1/integrations/oauth |
| Discovery | /api/v1/discovery |
| Discovery agents | /api/v1/discovery-agents |
| Projects | /api/v1/projects |
| Probes | /api/v1/probes |
Webhook endpoints are documented in webhooks and events.
Prerequisites: a running mmune backend, a Bearer JWT (write permission for any POST, PUT or DELETE call), and one of curl, Python 3 with requests, or Node 18+ for the examples. Authentication, error format, and pagination rules are in API overview and are not repeated here. The example host is https://mmune.example.com and <TOKEN> stands for your JWT.
How access works on these endpoints
A request to a route on this page needs a valid Bearer JWT with the read permission. Any method other than GET, HEAD or OPTIONS also needs the write permission, or the admin role. The sections below use these labels for the required authentication.
| Label | Meaning |
|---|---|
| read | Bearer JWT with read permission. |
| write | Bearer JWT with write permission or admin role. |
| admin | Bearer JWT with the admin role. |
| agent or write | No user JWT is required. The endpoint accepts either the named agent's own registration_token as the Bearer value, or a human JWT with write permission. |
| X-Probe-Key | No JWT is required. The X-Probe-Key header must match MMUNE_PROBE_KEY. |
Endpoint index
Integrations (/api/v1/integrations)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/integrations/supported | List the providers this build can detect and introspect. |
| GET | /api/v1/integrations/coverage | Report how many discovered endpoints are mapped, unidentified, or blocked. |
| POST | /api/v1/integrations/discover | Start a background discovery run. |
| GET | /api/v1/integrations/discovered | List discovered integrations, with filters. |
| GET | /api/v1/integrations/discovered/{discovered_id} | Get one discovered integration and its saved configurations. |
| POST | /api/v1/integrations/discovered/{discovered_id}/configure | Save credentials for a discovered integration. |
| POST | /api/v1/integrations/discovered/{discovered_id}/test | Test the connection using a saved or supplied configuration. |
| POST | /api/v1/integrations/discovered/{discovered_id}/register | Register the integration so it is monitored. |
| POST | /api/v1/integrations/discovered/{discovered_id}/introspect | Start a background schema introspection for one integration. |
| POST | /api/v1/integrations/introspect-all | Start a background introspection of every registered database. |
| GET | /api/v1/integrations/registered | List registered integrations. |
| GET | /api/v1/integrations/registered/{integration_id} | Get one registered integration. |
| GET | /api/v1/integrations/{integration_id}/tables | Page through the introspected tables of one integration. |
| POST | /api/v1/integrations/oauth/token/{provider} | Exchange an authorization code for a token directly. |
Discovery (/api/v1/discovery)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/discovery/schemas | List discovered database schemas. |
| POST | /api/v1/discovery/scan | Start a database discovery scan job. |
| GET | /api/v1/discovery/status/{scan_id} | Poll the status and logs of any background job. |
| POST | /api/v1/discovery/reintrospect/{integration_id} | Re-read the live schema of a registered integration. |
| GET | /api/v1/discovery/first-findings | List ranked, non-dismissed findings about the estate. |
| POST | /api/v1/discovery/first-findings/{finding_id}/dismiss | Dismiss one finding. |
Discovery agents (/api/v1/discovery-agents)
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/discovery-agents/register | Register an agent and mint its token. |
| GET | /api/v1/discovery-agents | List agents. |
| GET | /api/v1/discovery-agents/{agent_id} | Get one agent. |
| DELETE | /api/v1/discovery-agents/{agent_id} | Deregister an agent. |
| GET | /api/v1/discovery-agents/{agent_id}/health | Get agent health. |
| POST | /api/v1/discovery-agents/{agent_id}/heartbeat | Record an agent heartbeat. |
| GET | /api/v1/discovery-agents/{agent_id}/config | Fetch the agent configuration. |
| PUT | /api/v1/discovery-agents/{agent_id}/config | Replace the agent configuration. |
| POST | /api/v1/discovery-agents/{agent_id}/discoveries | Submit discovered integrations, optionally with schemas. |
| POST | /api/v1/discovery-agents/{agent_id}/kafka-flow-samples | Submit Kafka consumer lag readings. |
Projects (/api/v1/projects)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/projects/ | List projects with paging, search, and sorting. |
| POST | /api/v1/projects/ | Create a project. |
| GET | /api/v1/projects/{project_id} | Get one project. |
| PUT | /api/v1/projects/{project_id} | Update a project. |
| DELETE | /api/v1/projects/{project_id} | Delete a project. |
| POST | /api/v1/projects/{project_id}/start | Move a project to the discovery status. |
Probes (/api/v1/probes)
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/probes/heartbeat | Register or refresh a probe node. |
| GET | /api/v1/probes | List probe nodes, most recently seen first. |
Integrations
Integrations move through three states stored in configuration_state: discovered, configured, registered. Two identifiers are in play and they are not interchangeable. discovered_id is the integer primary key of a discovered row and is used in the /discovered/... paths. integration_id is a string and is used for /registered/..., /{integration_id}/tables, and the discovery re-introspect path. Both appear in the GET /discovered response. Ids are stable and opaque, so store them as given and do not parse them. The ids in the curl examples below are placeholders.
GET /api/v1/integrations/supported
Lists the providers supported by your installation.
Auth: read. No parameters.
Response 200:
| Field | Type | Description |
|---|---|---|
providers | array | One entry per provider, sorted by provider_id. |
providers[].provider_id | string | Canonical id, for example postgresql. |
providers[].display_name | string | Human name. |
providers[].category | string | One of oltp, olap, message_broker, cache, search, orchestrator, saas, other, infrastructure. |
providers[].tier | string | Coverage tier T1 to T4, computed at request time. |
providers[].capabilities | string[] | Sorted subset of fingerprint, credential_defaults, connection_probe, introspect, re_introspect, value_drift, containment. |
providers[].ports | integer[] | Default ports used for fingerprinting. |
providers[].driver_installed | boolean | Whether the provider's driver is available in this deployment. |
providers[].reachability | string | estate_local or internet. |
providers[].estate_cost_note | object | introspect_queries_per_table (integer), row_count_strategy (exact_count, catalog_estimate, none), billed_by_vendor (boolean). |
count | integer | Number of providers returned. |
bash
curl -s https://mmune.example.com/api/v1/integrations/supported \
-H "Authorization: Bearer <TOKEN>"GET /api/v1/integrations/coverage
Joins every discovered row with the provider registry and its persisted coverage state. A row with no coverage block counts only toward detected; it is pending, not mapped and not unidentified.
Auth: read. No parameters.
Response 200:
| Field | Type | Description |
|---|---|---|
endpoints | array | One entry per discovered row, ordered by id. |
endpoints[].id | integer | Discovered id. |
endpoints[].integration_id | string | Integration id. |
endpoints[].host, endpoints[].port | string or null, integer or null | Network location. |
endpoints[].provider | string | Provider name recorded at discovery. |
endpoints[].tier | string or null | Computed tier, null when the provider has no spec. |
endpoints[].state | string or null | Persisted coverage state, for example mapped, needs_connector, needs_driver, needs_credentials, unidentified. |
endpoints[].reason, endpoints[].detail, endpoints[].at | string or null | Reason code, free text, and timestamp of the state. |
summary.detected | integer | All discovered rows. |
summary.mapped, needs_connector, needs_driver, needs_credentials, unidentified | integer | Rows in each named state. |
summary.coverage_score | number or null | 1 - unidentified / detected, null when nothing has been scanned. |
bash
curl -s https://mmune.example.com/api/v1/integrations/coverage \
-H "Authorization: Bearer <TOKEN>"POST /api/v1/integrations/discover
Starts a background discovery run and returns immediately. Results are stored in the database. To follow progress, poll GET /api/v1/discovery/status/{scan_id} with the returned scan_id.
Auth: write.
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
hosts | string[] | no | null | IP addresses or CIDR ranges to probe. When empty, port scanning targets 127.0.0.1. |
ports | integer[] | no | null | Ports to probe. When empty, the known service ports are used. The MMUNE_SCAN_PORT_RANGE environment variable always adds to this set. |
api_endpoints | string[] | no | null | Known API base URLs for API discovery. |
openapi_specs | string[] | no | null | OpenAPI spec URLs. |
graphql_endpoints | string[] | no | null | GraphQL endpoint URLs. |
cloud_providers | string[] | no | null | Cloud providers to enumerate. |
cloud_regions | string[] | no | null | Cloud regions to enumerate. |
config_paths | string[] | no | null | Config file paths to analyze. |
methods | string[] | no | ["port_scanning", "api_discovery"] | Any of port_scanning, api_discovery, config_analysis, cloud_api, manual, import, agent_push. |
project_id | string | no | null | Project to associate the results with. |
An unrecognized value in methods causes the request to fail.
Response 200:
| Field | Type | Description |
|---|---|---|
message | string | Discovery started. |
scan_id | string | Job id to poll. |
status | string | RUNNING. |
When the job completes, the results field of the status response holds {"discovered": [...], "count": n}. Each discovered item has integration_id, integration_type, integration_pattern, provider_name, host, port, endpoint, confidence, discovery_method, metadata, and discovered_at. It does not carry the integer discovered_id; get that from GET /discovered.
bash
curl -s -X POST https://mmune.example.com/api/v1/integrations/discover \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"hosts": ["10.0.1.0/28"], "ports": [5432, 3306], "methods": ["port_scanning"]}'GET /api/v1/integrations/discovered
Lists discovered integrations, newest last_seen_at first.
Auth: read.
Query parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
project_id | string | no | null | Only rows for this project. |
integration_type | string | no | null | One of database, saas_platform, api, file_system, message_queue, cloud_service, erp_system, legacy_system, custom_app, data_warehouse, streaming, microservice. |
integration_pattern | string | no | null | One of rest_api, sql_protocol, file_based, message_based, graphql_api, soap_api, streaming, batch, webhook. |
provider_name | string | no | null | Provider id such as postgresql. |
configuration_state | string | no | null | discovered, configured, or registered. |
Response 200: {"discovered": [...], "count": n}. Each item has id, integration_id, integration_type, integration_pattern, provider_name, host, port, endpoint, discovery_method, confidence, discovery_metadata, configuration_state, organization_id, project_id, first_seen_at, last_seen_at, configured_at, registered_at.
Status codes: 200, 400 when integration_type or integration_pattern is not a valid enum value (Invalid filter value: ...).
bash
curl -s "https://mmune.example.com/api/v1/integrations/discovered?provider_name=postgresql&configuration_state=discovered" \
-H "Authorization: Bearer <TOKEN>"GET /api/v1/integrations/discovered/
Returns one discovered row with its saved configurations. Secrets are not included in the configuration entries.
Auth: read.
Path parameters: discovered_id (integer, required).
Response 200: the same fields as one item of the list above, plus configurations, an array of id, discovered_integration_id, config_type, credentials_stored_in, test_status (untested, passed, failed), test_error, tested_at, created_at, updated_at.
Status codes: 200, 404 (Discovered integration not found: {id}).
bash
curl -s https://mmune.example.com/api/v1/integrations/discovered/12 \
-H "Authorization: Bearer <TOKEN>"POST /api/v1/integrations/discovered/{discovered_id}/configure
Builds and stores a configuration for a discovered integration and sets its state to configured. The shape of credentials depends on the integration pattern; see Configuration shapes.
Auth: write.
Path parameters: discovered_id (integer, required).
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
credentials | object | no | {} | Connection credentials and options. |
auth_config | object or null | no | null | Authentication block, mainly for REST APIs. Either nested ({"method": "basic_auth", "config": {...}}) or flat ({"username": "...", "password": "..."}). |
Response 200:
| Field | Type | Description |
|---|---|---|
message | string | Integration configured successfully. |
configuration_id | integer | Id to pass as config_id to test or register. |
discovered_id | integer | Echo of the path value. |
Status codes: 200, 400 for any failure, including an unknown discovered_id and a database row with no host or port (Failed to configure integration: ...).
bash
curl -s -X POST https://mmune.example.com/api/v1/integrations/discovered/12/configure \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"credentials": {"username": "mmune_reader", "password": "<DB_PASSWORD>", "database": "orders"}}'POST /api/v1/integrations/discovered/{discovered_id}/test
Runs a connection test. It uses the latest saved configuration, a specific config_id, or an inline config_override. The result is also written back to the saved configuration as test_status.
Auth: write.
Path parameters: discovered_id (integer, required).
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
config_id | integer or null | no | null | Configuration to test. The latest one is used when omitted. |
config_override | object or null | no | null | Inline configuration, shaped {"credentials": {...}, "auth_config": {...}}. Takes precedence over saved configurations and works before configure has been called. |
Response 200: {"message": "Connection test completed", "test_result": {...}}. test_result has success (boolean), connection_time_ms, authentication_time_ms, error, error_type, details (object), and tested_at. A failed login is reported inside test_result with success: false, not as an HTTP error.
Status codes: 200, 400 (Connection test failed: ...) for an unknown id or when neither a saved configuration nor config_override exists.
bash
curl -s -X POST https://mmune.example.com/api/v1/integrations/discovered/12/test \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{}'POST /api/v1/integrations/discovered/{discovered_id}/register
Registers the integration so the watchdog and the agent chain monitor it. Registration consumes a license seat unless the row is already registered. On success mmune also writes a lineage source node for the integration. If the lineage write fails, the registration still succeeds.
Auth: write.
Path parameters: discovered_id (integer, required).
Query parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
config_id | integer | no | null | Configuration to register with. The latest one is used when omitted. |
Response 200:
| Field | Type | Description |
|---|---|---|
message | string | Integration registered successfully. |
integration_id | string | Id used in /registered/... and lineage. |
provider_name | string | Provider id. |
capabilities | string[] | Connector capabilities, for example read_data, schema_discovery. |
Status codes: 200, 403 with detail.code of license_cap_exceeded (also carries message, max_systems, current) or license_blocked (also carries message), 400 for any other failure (Failed to register integration: ...).
bash
curl -s -X POST "https://mmune.example.com/api/v1/integrations/discovered/12/register?config_id=7" \
-H "Authorization: Bearer <TOKEN>"POST /api/v1/integrations/discovered/{discovered_id}/introspect
Connects to a configured integration, pulls table-level schema, creates lineage nodes (source, database, table), and flags PII columns. It runs in the background. Poll the returned scan_id at the discovery status endpoint, or read the lineage graph.
Auth: write. No body.
Path parameters: discovered_id (integer, required).
Response 200: {"message": "Introspection started", "scan_id": "<uuid>", "discovered_id": 12}. The completed job result is the introspection result, which includes tables_discovered and integration_id. A failure sets the job to FAILED with an error; the HTTP call itself still returns 200.
bash
curl -s -X POST https://mmune.example.com/api/v1/integrations/discovered/12/introspect \
-H "Authorization: Bearer <TOKEN>"POST /api/v1/integrations/introspect-all
Introspects every configured or registered database integration in the background.
Auth: write. No body, no parameters.
Response 200: {"message": "Bulk introspection started", "scan_id": "<uuid>"}. The completed job result is {"results": [...], "total_tables": n}, where an entry that failed carries an error key.
bash
curl -s -X POST https://mmune.example.com/api/v1/integrations/introspect-all \
-H "Authorization: Bearer <TOKEN>"GET /api/v1/integrations/registered
Lists the integrations that are currently registered.
Auth: read.
Response 200: {"registered": [...], "count": n, "statistics": {...}}. Each registered item has integration_id, integration_type, integration_pattern, provider_name, version, capabilities, metadata, registered_at.
statistics is an object of summary counts for the registry.
bash
curl -s https://mmune.example.com/api/v1/integrations/registered \
-H "Authorization: Bearer <TOKEN>"GET /api/v1/integrations/registered/
Auth: read.
Path parameters: integration_id (string, required).
Response 200: {"integration": {...}, "statistics": {...}} with the same item shape as the list.
Status codes: 200, 404 (Registered integration not found: {id}).
bash
curl -s https://mmune.example.com/api/v1/integrations/registered/3f2c9a40-0d7e-4b1c-9a55-1c2f6e7d8a90 \
-H "Authorization: Bearer <TOKEN>"GET /api/v1/integrations/{integration_id}/tables
Returns a scoped, paged inventory of tables mmune has introspected for one integration. It reads only the lineage mmune has already stored and never queries your database. Each table carries a watch state.
Auth: read.
Path parameters: integration_id (string, required). This is the string id, not the integer discovered_id.
Query parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
limit | integer | no | 50 | Page size, 1 to 200. |
offset | integer | no | 0 | Rows to skip, 0 or more. |
q | string | no | null | Case-insensitive substring match on the table node key, up to 200 characters. |
Response 200:
| Field | Type | Description |
|---|---|---|
integration_id, provider | string | The integration and its provider name. |
databases | array | Per database: database, node_key, tables_introspected. |
tables_introspected_total | integer or null | Sum of table counts across databases, null if none recorded. |
table_nodes_in_lineage | integer | Table nodes for this integration, ignoring q. |
total, limit, offset | integer | Count matching q and the paging values used. |
tables | array | Per table: node_key, database, schema, table, column_count, row_count_source (unknown when absent), watch. |
watch_window | object | provider_supported, sampler_enabled, integration_watched, directly_reachable, max_tables_per_window, window_count, active_window_index, tables_in_active_window, rotation_period_seconds, next_rotation_in_seconds. |
Baselines are empty until the watchdog has run.
Status codes: 200, 404 (Integration not found: {id}).
bash
curl -s "https://mmune.example.com/api/v1/integrations/3f2c9a40-0d7e-4b1c-9a55-1c2f6e7d8a90/tables?limit=20&q=order" \
-H "Authorization: Bearer <TOKEN>"Integration OAuth
POST /api/v1/integrations/oauth/token/
Exchanges an authorization code directly, using client credentials you supply, and saves the resulting token as the integration configuration. Supported providers for this call are salesforce, servicenow, and hubspot.
Auth: write.
All inputs are query parameters, not a JSON body.
Path parameters: provider (string, required).
Query parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
code | string | yes | none | Authorization code. |
discovered_id | integer | yes | none | Discovered integration to configure. |
client_id | string | yes | none | OAuth client id. |
client_secret | string | yes | none | OAuth client secret. |
redirect_uri | string | no | null | Redirect URI used when the code was issued. |
Response 200: message, configuration_id, discovered_id, token_type, expires_at (ISO 8601 or null).
Status codes: 200, or an error response whose detail starts with Token exchange failed: when the exchange cannot be completed.
bash
curl -s -X POST "https://mmune.example.com/api/v1/integrations/oauth/token/hubspot?code=<AUTH_CODE>&discovered_id=12&client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>" \
-H "Authorization: Bearer <TOKEN>"Discovery
GET /api/v1/discovery/schemas
Returns the discovered schemas.
Auth: read. No parameters.
Response 200:
| Field | Type | Description |
|---|---|---|
schemas | array | Schema entries. For introspection: integration_id, provider, host, port, database, tables, introspected_at. Each table has schema, table, database, row_count, column_count, columns, has_pii, pii_fields. |
count | integer | Number of entries. |
source | string | introspection, inventory, or live_scan. |
bash
curl -s https://mmune.example.com/api/v1/discovery/schemas \
-H "Authorization: Bearer <TOKEN>"POST /api/v1/discovery/scan
Starts a database discovery scan as a background job and records the discovered databases in the inventory.
Auth: write.
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
target | string | no | "" | A host, or a comma-separated list of hosts. With an empty value the scan covers localhost. |
ports | integer[] | no | [] | Ports to check. |
Response 200: {"message": "Discovery scan started", "scan_id": "<uuid>", "status": "RUNNING"}. The completed job result is {"schemas": [...], "count": n}.
bash
curl -s -X POST https://mmune.example.com/api/v1/discovery/scan \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"target": "10.0.1.15,10.0.1.16", "ports": [5432, 1433]}'GET /api/v1/discovery/status/
Returns the status and logs of any background job started by discover, scan, introspect, or introspect-all. Pass back next_cursor as cursor to receive only new log lines.
Auth: read.
Path parameters: scan_id (string, required).
Query parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
cursor | integer | no | 0 | Log offset, 0 or more. |
Response 200:
| Field | Type | Description |
|---|---|---|
status | string | RUNNING, COMPLETED, or FAILED. |
logs | string[] | Log lines from cursor onward. |
next_cursor | integer | Value to send as cursor on the next poll. |
results | any | Job result once completed, otherwise null. |
created_at, updated_at | string | ISO 8601 timestamps. |
error | string or null | Failure reason when status is FAILED. |
An unknown scan_id returns HTTP 200 with the body {"error": "Scan ID not found"}, not a 404. Check for the error key without a status key.
bash
curl -s "https://mmune.example.com/api/v1/discovery/status/6b0c7a1e-5d3f-4e8a-9b21-0f4d2c1a7e55?cursor=0" \
-H "Authorization: Bearer <TOKEN>"POST /api/v1/discovery/reintrospect/
Forces a fresh introspection of a registered integration and rewrites its table lineage nodes, so the watchdog has a new snapshot to compare for drift. Use this call to refresh the schema after a change. It then recomputes first findings on a best-effort basis.
Auth: write. No body.
Path parameters: integration_id (string, required).
Response 200: integration_id, reintrospected (boolean, false when the run was skipped), result (introspection result object), table_count (integer).
Status codes: 200, 404 (Integration not found: {id}).
bash
curl -s -X POST https://mmune.example.com/api/v1/discovery/reintrospect/3f2c9a40-0d7e-4b1c-9a55-1c2f6e7d8a90 \
-H "Authorization: Bearer <TOKEN>"GET /api/v1/discovery/first-findings
Returns ranked findings about the estate that have not been dismissed. Ranking is deterministic: severity class first, then blast radius, then recency.
Auth: read.
Query parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
limit | integer | no | 50 | Maximum findings, 1 to 500. |
Response 200:
| Field | Type | Description |
|---|---|---|
findings | array | Each has id, kind, integration_id, object_key, statement, numbers (object), severity, blast_radius, computed_at, dismissed. |
count | integer | Findings returned. |
estate_summary | object | integrations_discovered and tables_introspected, useful for an empty state. |
bash
curl -s "https://mmune.example.com/api/v1/discovery/first-findings?limit=10" \
-H "Authorization: Bearer <TOKEN>"POST /api/v1/discovery/first-findings/{finding_id}/dismiss
Marks a finding dismissed so it leaves the ranked list.
Auth: write. No body.
Path parameters: finding_id (integer, required).
Response 200: the finding object, as listed above, with dismissed set to true.
Status codes: 200, 404 (Finding not found: {id}).
bash
curl -s -X POST https://mmune.example.com/api/v1/discovery/first-findings/41/dismiss \
-H "Authorization: Bearer <TOKEN>"Discovery agents
A discovery agent is a process installed inside a network that mmune cannot reach directly. It registers once, then sends heartbeats about every 30 seconds, pulls its configuration, and pushes what it finds. Agent endpoints marked "agent or write" accept the agent's own registration_token as the Bearer value. That token only authenticates calls whose {agent_id} is the agent it was issued to. A human JWT is accepted on those routes only with write permission.
POST /api/v1/discovery-agents/register
Registers an agent and returns its token. Keep the token; it is the agent's credential.
Auth: admin.
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | yes | none | Unique agent identifier. |
name | string | no | agent_id | Display name. |
version | string | no | unknown | Agent version. |
platform | string | no | unknown | For example linux, windows, docker. |
capabilities | string[] | no | [] | Capabilities the agent reports. |
Response 200: agent_id, status, registration_token, config, registered_at. The initial config holds the server defaults: heartbeat_interval 30, discovery_methods ["port_scan", "config_files"], scan_interval 300, max_concurrent_scans 10, timeout 30.
Status codes: 200, 400 (invalid registration), 500.
bash
curl -s -X POST https://mmune.example.com/api/v1/discovery-agents/register \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"agent_id": "dmz-agent-01", "name": "DMZ agent", "platform": "docker", "capabilities": ["port_scan"]}'GET /api/v1/discovery-agents
Auth: read.
Query parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
status | string | no | null | Only agents with this status. |
Response 200: {"agents": [...], "count": n}. Each agent has id, organization_id, name, version, platform, status, last_heartbeat, config, metadata, created_at, updated_at. The registration token is not included.
bash
curl -s "https://mmune.example.com/api/v1/discovery-agents?status=healthy" \
-H "Authorization: Bearer <TOKEN>"GET /api/v1/discovery-agents/
Auth: read.
Path parameters: agent_id (string, required).
Response 200: one agent object as above. Status codes: 200, 404 (agent not found).
bash
curl -s https://mmune.example.com/api/v1/discovery-agents/dmz-agent-01 \
-H "Authorization: Bearer <TOKEN>"DELETE /api/v1/discovery-agents/
Revokes the agent.
Auth: admin.
Path parameters: agent_id (string, required).
Response 200: {"status": "ok", "message": "Agent deregistered"}. Status codes: 200, 404.
bash
curl -s -X DELETE https://mmune.example.com/api/v1/discovery-agents/dmz-agent-01 \
-H "Authorization: Bearer <TOKEN>"GET /api/v1/discovery-agents/{agent_id}/health
Auth: read.
Path parameters: agent_id (string, required).
Response 200: agent_id, status (healthy, unhealthy, degraded, or offline), last_heartbeat (ISO 8601 or null), missed_heartbeats (integer), error_message (string or null), metrics (object). Status codes: 200, 404.
bash
curl -s https://mmune.example.com/api/v1/discovery-agents/dmz-agent-01/health \
-H "Authorization: Bearer <TOKEN>"POST /api/v1/discovery-agents/{agent_id}/heartbeat
Auth: agent or write.
Path parameters: agent_id (string, required).
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
metrics | object or null | no | null | Free-form agent metrics such as CPU and memory. |
Response 200: {"status": "ok", "message": "Heartbeat received"}. Status codes: 200, 401 (no credential), 403 (JWT without write), 404 (unknown agent).
bash
curl -s -X POST https://mmune.example.com/api/v1/discovery-agents/dmz-agent-01/heartbeat \
-H "Authorization: Bearer <AGENT_REGISTRATION_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"metrics": {"cpu_percent": 12.5, "memory_mb": 210}}'GET /api/v1/discovery-agents/{agent_id}/config
Agents fetch their configuration on startup and after a change notice.
Auth: agent or write. A viewer JWT is refused here even though the method is GET.
Path parameters: agent_id (string, required).
Response 200: the configuration object (see the defaults under register). Status codes: 200, 401, 403, 404.
bash
curl -s https://mmune.example.com/api/v1/discovery-agents/dmz-agent-01/config \
-H "Authorization: Bearer <AGENT_REGISTRATION_TOKEN>"PUT /api/v1/discovery-agents/{agent_id}/config
Replaces the stored configuration. The agent picks it up on its next heartbeat or config fetch.
Auth: agent or write.
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
config | object | yes | none | Complete configuration dictionary. |
Response 200: {"status": "ok", "message": "Configuration updated"}. Status codes: 200, 401, 403, 404.
bash
curl -s -X PUT https://mmune.example.com/api/v1/discovery-agents/dmz-agent-01/config \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"config": {"heartbeat_interval": 30, "scan_interval": 600, "max_concurrent_scans": 5, "timeout": 30}}'POST /api/v1/discovery-agents/{agent_id}/discoveries
Receives a batch of discovered integrations. Duplicates are matched on endpoint, host and port; a duplicate with a higher confidence_score updates the existing row. If an item carries agent_schema, mmune writes the lineage nodes from it and starts its follow-on analysis, so the agent can report schemas for databases mmune cannot reach itself.
Auth: agent or write.
Path parameters: agent_id (string, required).
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
discoveries | array | yes | none | Items described next. |
discoveries[].integration_type | string | yes | none | Provider id such as postgresql. It becomes provider_name; the category and pattern are looked up from the provider registry, with custom_app and rest_api as the fallback for an unknown provider. |
discoveries[].name | string | yes | none | Integration name. |
discoveries[].confidence_score | number | yes | none | 0.0 to 1.0. |
discoveries[].discovery_method | string | yes | none | For example port_scan, config_file. |
discoveries[].endpoint | string | no | null | host:port or URL. |
discoveries[].host | string | no | null | Host address. |
discoveries[].port | integer | no | null | Port number. |
discoveries[].metadata | object | no | {} | Extra details. |
discoveries[].agent_schema | object | no | null | Schema snapshot, described next. |
agent_schema.database | string | no | default | Database name. |
agent_schema.tables | array | no | [] | Tables. |
tables[].table | string | yes | none | Table name. |
tables[].db_schema | string | no | database name | Schema name. |
tables[].row_count | integer | no | -1 | Row count, -1 when unknown. |
tables[].columns | array | no | [] | Columns. |
columns[].name | string | yes | none | Column name. |
columns[].type | string | no | UNKNOWN | Column type. |
columns[].nullable | boolean | no | true | Nullability. |
columns[].pii | boolean | no | false | Whether the agent flagged the column as PII. |
Response 200:
| Field | Type | Description |
|---|---|---|
status | string | ok. |
message | string | Processed {n} discoveries. |
stored | integer | New rows created. |
duplicates | integer | Items that matched an existing row. |
errors | integer | Items that failed and were skipped. |
lineage_tables_upserted | integer | Table nodes written from agent_schema. |
Status codes: 200, 401, 403, 422 (invalid body), 500.
bash
curl -s -X POST https://mmune.example.com/api/v1/discovery-agents/dmz-agent-01/discoveries \
-H "Authorization: Bearer <AGENT_REGISTRATION_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"discoveries": [{
"integration_type": "postgresql",
"name": "orders-db",
"host": "10.0.1.15",
"port": 5432,
"confidence_score": 0.95,
"discovery_method": "port_scan",
"agent_schema": {
"database": "orders",
"tables": [{"table": "customers", "db_schema": "public", "row_count": 1200,
"columns": [{"name": "email", "type": "text", "nullable": false, "pii": true}]}]
}
}]
}'POST /api/v1/discovery-agents/{agent_id}/kafka-flow-samples
Receives raw Kafka consumer lag readings from an agent that can see a cluster mmune cannot. The agent sends raw positions only. Classification of stuck or orphaned partitions happens on the server, with the same logic used for directly reachable Kafka clusters.
Auth: agent or write.
Path parameters: agent_id (string, required).
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
host | string | yes | none | Kafka host. It is matched against previously discovered integrations by host and port. |
port | integer | yes | none | Kafka port. |
lags | array | no | [] | Readings. |
lags[].topic | string | yes | none | Topic name. |
lags[].partition | integer | yes | none | Partition number. |
lags[].group | string | yes | none | Consumer group. |
lags[].high_water_mark | integer | yes | none | Latest offset in the partition. |
lags[].committed_offset | integer | yes | none | Offset the group has committed. |
lags[].has_consumer | boolean | no | true | Whether the group has an active consumer. |
Response 200 has one of three shapes, keyed by status:
status | Other fields | Meaning |
|---|---|---|
skipped | reason: "integration_not_discovered" | No discovered integration matches host and port yet. Not an error; push again after the discovery push lands. |
processed_no_session | findings_count | Findings were computed, but no monitoring session is attached, so no alerts were raised. |
processed | findings_count, dispatch_count | Findings applied to session health, the detection log, and outbound alerts. |
Status codes: 200, 401, 403, 422, 500.
bash
curl -s -X POST https://mmune.example.com/api/v1/discovery-agents/dmz-agent-01/kafka-flow-samples \
-H "Authorization: Bearer <AGENT_REGISTRATION_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"host": "10.0.2.20", "port": 9092, "lags": [{"topic": "orders", "partition": 0, "group": "billing", "high_water_mark": 5400, "committed_offset": 5100, "has_consumer": true}]}'Projects
A project is a stored migration configuration with a status, metrics, and progress. Every project endpoint requires an authenticated, active user. Use the trailing slash on the collection path (/api/v1/projects/).
Types used below:
| Object | Fields |
|---|---|
| Database config | type (string, required), host, port (integer), database, username, password, connection_string (all optional strings except port), options (object, default {}). |
| Migration config | batch_size (integer 1 to 10000, default 1000), parallel_tasks (integer 1 to 32, default 4), validate_data (boolean, default true), preserve_schema (boolean, default true), custom_mappings (object, default {}). |
| Agents config | Five blocks, each with enabled (boolean, default true) plus: discovery (scan_depth shallow or deep, default deep; include_views true; include_procedures true), analysis (ai_provider gemini, openai, or claude, default gemini; analysis_depth basic, detailed, or comprehensive, default detailed), mapping (auto_mapping true; confidence_threshold 0.0 to 1.0, default 0.8), validation (data_quality_checks, referential_integrity, business_rules, all true), execution (dry_run false; rollback_enabled true). |
| Project config | source (database config, required), target (database config, required), migration (migration config), agents (agents config). |
| Project | id, name, description, status, config, metrics, progress, created_at, updated_at, created_by (the creator's email), tags. |
metrics | tables_discovered, tables_analyzed, tables_mapped, tables_validated, tables_migrated, records_processed, errors_found, warnings_found (integers), execution_time (number), data_volume (integer). All default to 0. |
progress | discovery, analysis, mapping, validation, execution, each a number from 0 to 100, default 0. |
The stored config is returned exactly as saved. Do not include passwords.
GET /api/v1/projects/
Auth: read.
Query parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
page | integer | no | 1 | Page number, minimum 1. |
page_size | integer | no | 10 | Items per page, clamped to 1 to 100. |
status | string | no | null | Exact status match. |
search | string | no | null | Case-insensitive match on name or description. |
sort_by | string | no | null | name, created_at, or updated_at. Other values leave the order unchanged. |
sort_order | string | no | asc | asc or desc. |
Response 200: {"projects": [project, ...], "total": n, "page": n, "page_size": n}.
bash
curl -s "https://mmune.example.com/api/v1/projects/?page=1&page_size=20&sort_by=created_at&sort_order=desc" \
-H "Authorization: Bearer <TOKEN>"POST /api/v1/projects/
Creates a project in the draft status.
Auth: write.
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | yes | none | 3 to 100 characters. |
description | string | yes | none | 10 to 1000 characters. |
config | project config | yes | none | Only source and target are required inside it. |
tags | string[] | no | [] | Labels. |
Response 201: the project. Status codes: 201, 422 for validation errors.
bash
curl -s -X POST https://mmune.example.com/api/v1/projects/ \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"name": "Orders migration",
"description": "Move the orders schema to the new cluster.",
"config": {
"source": {"type": "postgresql", "host": "10.0.1.15", "port": 5432, "database": "orders"},
"target": {"type": "postgresql", "host": "10.0.3.10", "port": 5432, "database": "orders_new"}
},
"tags": ["orders"]
}'GET /api/v1/projects/
Auth: read. Path parameters: project_id (string, required). Response 200: the project. Status codes: 200, 404 (Project {id} not found).
bash
curl -s https://mmune.example.com/api/v1/projects/8d1f6c0e-2b7a-4c53-b9d4-6a1e0f3c9b72 \
-H "Authorization: Bearer <TOKEN>"PUT /api/v1/projects/
Updates only the fields you send. Sending config replaces the whole stored config.
Auth: write. Path parameters: project_id (string, required).
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | no | null | 3 to 100 characters. |
description | string | no | null | 10 to 1000 characters. |
config | project config | no | null | Full replacement config. |
tags | string[] | no | null | Full replacement tag list. |
status | string | no | null | Free-form status string. |
Response 200: the updated project. Status codes: 200, 404, 422.
bash
curl -s -X PUT https://mmune.example.com/api/v1/projects/8d1f6c0e-2b7a-4c53-b9d4-6a1e0f3c9b72 \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"tags": ["orders", "q4"]}'DELETE /api/v1/projects/
Auth: write. Path parameters: project_id (string, required). Response 200: {"message": "Project {id} deleted successfully"}. Status codes: 200, 404.
bash
curl -s -X DELETE https://mmune.example.com/api/v1/projects/8d1f6c0e-2b7a-4c53-b9d4-6a1e0f3c9b72 \
-H "Authorization: Bearer <TOKEN>"POST /api/v1/projects/{project_id}/start
Sets the project status to discovery and updates updated_at.
Auth: write. Path parameters: project_id (string, required). Response 200: {"message": "Project {id} started successfully"}. Status codes: 200, 400 (Project is already completed), 404.
bash
curl -s -X POST https://mmune.example.com/api/v1/projects/8d1f6c0e-2b7a-4c53-b9d4-6a1e0f3c9b72/start \
-H "Authorization: Bearer <TOKEN>"Probes
Probes are lightweight nodes that report in to the central backend. The heartbeat is the one route here that does not use a JWT.
POST /api/v1/probes/heartbeat
Creates the probe record on first sight and refreshes hostname, capabilities, and last_seen_at on every later call. The shared secret is the value of the MMUNE_PROBE_KEY environment variable on the backend. If that variable is unset, the endpoint rejects every request.
Auth: X-Probe-Key.
Headers:
| Name | Required | Description |
|---|---|---|
X-Probe-Key | yes | Must equal the backend's MMUNE_PROBE_KEY. |
Body:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
node_id | string | yes | none | Unique probe id. The record is keyed on it. |
hostname | string | no | null | Probe host name. |
capabilities | object | no | {} | Free-form capability flags, for example {"discovery": true}. |
Response 200: {"status": "ok"}.
Status codes: 200, 401 (Invalid probe key, including a missing header), 503 (Probe enrollment is not configured (MMUNE_PROBE_KEY missing)), 422 (invalid body).
bash
curl -s -X POST https://mmune.example.com/api/v1/probes/heartbeat \
-H "X-Probe-Key: <PROBE_KEY>" \
-H "Content-Type: application/json" \
-d '{"node_id": "probe-dmz-01", "hostname": "dmz-probe-01", "capabilities": {"discovery": true}}'GET /api/v1/probes
Auth: read.
Query parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
page | integer | no | 1 | Page number. |
page_size | integer | no | 10 | Items per page, 1 to 100. |
Response 200: {"probes": [...], "total": n, "page": n, "page_size": n}, newest last_seen_at first. Each probe has node_id, hostname, capabilities, first_seen_at, last_seen_at.
bash
curl -s "https://mmune.example.com/api/v1/probes?page=1&page_size=50" \
-H "Authorization: Bearer <TOKEN>"Supported connector types
GET /api/v1/integrations/supported is the source of truth for a given build, because tiers and availability are computed when you ask. The table below lists the providers this release supports. A provider can be fingerprinted from its ports or banner without being introspected; the last column says which.
provider_id | Aliases | Name | Default ports | Introspects schema |
|---|---|---|---|---|
postgresql | none | PostgreSQL | 5432 | yes |
mysql | none | MySQL | 3306 | yes |
mariadb | none | MariaDB | none | yes |
sqlserver | sql_server, mssql | SQL Server | 1433 | yes |
oracle | none | Oracle | 1521 | yes |
mongodb | none | MongoDB | 27017 | yes |
redis | redis-oss, valkey | Redis | 6379 | yes |
cassandra | scylla | Apache Cassandra | 9042 | yes |
elasticsearch | elastic, opensearch | Elasticsearch | 9200 | yes |
kafka | none | Apache Kafka | 9092 | yes |
rabbitmq | none | RabbitMQ | 5672 | yes |
airflow | apache_airflow, apache-airflow | Apache Airflow | none | yes |
snowflake | sf | Snowflake | none | yes |
sap_hana | sap | SAP HANA | 39013 | yes |
sap_rfc | none | SAP RFC | none | yes |
tibco | none | TIBCO | none | yes |
db2_zos | db2 | IBM DB2 z/OS | 50000 | no |
teradata | none | Teradata | 1025 | no |
activemq | none | Apache ActiveMQ | 61616 | no |
solr | none | Apache Solr | none | no |
http | none | HTTP | 80, 443, 8080 | no |
ssh | none | SSH | 22 | no |
smtp | none | SMTP | 25 | no |
ftp, ftp_or_smtp, imap, pop3, vnc, tls | none | FTP, FTP or SMTP (ambiguous banner), IMAP, POP3, VNC, TLS (certificate-only) | none | no |
grafana, splunk | none | Grafana, Splunk | none | no |
Providers marked "no" are detected and reported in /coverage as needing a connector or marked as infrastructure. They cannot be registered for schema monitoring.
OAuth-based SaaS platforms are configured through direct token exchange, which supports only salesforce, servicenow, and hubspot.
Configuration shapes by connector type
The credentials object in configure and in config_override is read according to the integration pattern recorded at discovery. Unknown keys are not rejected.
SQL protocol (databases)
Applies to every provider whose integration pattern is sql_protocol: postgresql, mysql, mariadb, sqlserver, oracle, mongodb, redis, sap_hana, snowflake and cassandra. db2_zos and teradata also declare this pattern, but they cannot be registered for schema monitoring. The discovered row must have a host and a port or configure fails.
| Key | Type | Default | Description |
|---|---|---|---|
username | string | null | Login. user is also read by the introspector. |
password | string | null | Password. |
database | string | provider default | Database name. db is accepted by the introspector. Defaults when omitted: postgres for PostgreSQL, master for SQL Server, empty for MySQL and Oracle. |
connection_timeout | integer | 30 | Seconds. |
query_timeout | integer | 300 | Seconds. |
ssl_enabled | boolean | false | Use TLS. |
ssl_config | object | {} | TLS options. |
connection_pool_size | integer | 10 | Pool size. |
max_connections | integer | 50 | Connection ceiling. |
additional_params | object | {} | Provider-specific extras. Any key not listed here is moved into additional_params. |
Provider-specific keys:
| Provider | Key | Description |
|---|---|---|
oracle | service_name | Service name. Falls back to database, then MMUNE_ORACLE_SERVICE, then FREEPDB1. |
oracle | schemas | Comma-separated string or list that bounds catalog reads. Falls back to MMUNE_ORACLE_SCHEMAS. |
sap_hana | schemas or schema | Schemas to introspect. Falls back to MMUNE_HANA_SCHEMAS, then database. |
sap_hana | encrypt | TLS on the connection. Default true. MMUNE_HANA_ENCRYPT overrides. |
sap_hana | ssl_validate | Validate the server certificate. Defaults to true in production mode. MMUNE_HANA_SSL_VALIDATE overrides. |
sap_hana | ca_cert | PEM trust store for a private CA. MMUNE_HANA_CA_CERT supplies it from the environment. |
sap_hana | tenant | Multi-tenant database name, for example HXE. MMUNE_HANA_TENANT supplies it from the environment. |
HANA keys can sit in credentials or auth_config; credentials wins.
SAP RFC
Read from credentials or auth_config, with environment fallbacks. Both rfc_host and rfc_sysnr are required, or RFC discovery is skipped.
| Key | Description |
|---|---|
rfc_host | Application server host. Falls back to MMUNE_SAP_RFC_HOST. |
rfc_sysnr | System number. Falls back to MMUNE_SAP_RFC_SYSNR. |
rfc_client | Client. Falls back to MMUNE_SAP_RFC_CLIENT, then 000. |
rfc_user, rfc_password | RFC login. Fall back to the integration's username and password. |
rfc_ashost, rfc_mshost, rfc_group, rfc_sysid | Optional load-balancing settings. |
REST API and GraphQL
For these patterns the base URL is the discovered endpoint, or is built from host and port. Authentication is resolved from auth_config first, then from credentials.
| Key | Where | Default | Description |
|---|---|---|---|
auth_config.method | auth_config | inferred | One of none, basic_auth, api_key, oauth2, jwt, bearer_token. Inferred from the keys present when omitted. |
api_key, header_name | credentials | header X-API-Key | API key authentication. |
username, password | credentials or auth_config | none | Basic authentication. |
access_token (or token, bearer_token in auth_config) | either | none | Bearer token. |
client_id, client_secret, token_url, grant_type | credentials | token URL {base_url}/oauth/token, grant client_credentials | OAuth 2.0 client credentials. |
default_headers | credentials | {} | Extra request headers. |
timeout | credentials | 30 | Seconds. |
max_retries | credentials | 3 | Retry count. |
retry_delay | credentials | 1.0 | Seconds between retries. |
enable_pagination, pagination_config | credentials | true, {} | Pagination handling. |
rate_limit | credentials | none | Rate limit settings for requests to the API. |
Airflow reads token or bearer_token, or username and password, plus use_tls, verify_tls (default true), base_path, timeout, dag_page_size, and dag_hard_cap. Elasticsearch reads username and password from credentials and use_tls at the top level. Cassandra reads username and password from credentials. Snowflake reads account (default the discovered host), database (required), username, and password from credentials, and warehouse and role from additional_params.
TIBCO
TIBCO is read over SSH with a key and no password. The fields can sit at the top level or inside config or credentials.
| Key | Default | Description |
|---|---|---|
username (or user) | <ssh-user> | SSH user. |
ssh_key_path | MMUNE_TIBCO_SSH_KEY_PATH | Private key path. Required; without it introspection is skipped with a credentials_missing reason. |
ssh_port | 22 | SSH port. |
ssh_key_passphrase | none | Key passphrase. |
known_hosts_policy | true | Reject unknown host keys. |
known_hosts_path | MMUNE_TIBCO_KNOWN_HOSTS | Known hosts file. |
enabled_products | all | Products to harvest. |
log_config | none | Elastic log source: endpoint, index_pattern (default tibco-logs-*), username, password, api_key, verify_certs (default true), ca_cert_path. |
ems_admin | none | Live EMS telemetry: server_url (default tcp://localhost:7222), admin_user, admin_password, tibemsadmin_path, plus SSH overrides. |
sink_dbs | [] | Databases fed by TIBCO, each with name, provider, host, port, database, username, password, freshness_tables, timestamp_column. |
Example configure call for a PostgreSQL database with a custom timeout and TLS:
bash
curl -s -X POST https://mmune.example.com/api/v1/integrations/discovered/12/configure \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"credentials": {"username": "mmune_reader", "password": "<DB_PASSWORD>", "database": "orders", "ssl_enabled": true, "connection_timeout": 15}}'Workflow 1: discover and register an integration end to end
The sequence is: start discovery, poll the job, list discovered rows to find the integer discovered_id, configure, test, register, then introspect.
Python (requests)
python
import time
import requests
BASE = "https://mmune.example.com/api/v1"
HEADERS = {"Authorization": "Bearer <TOKEN>"}
def call(method, path, **kwargs):
resp = requests.request(method, f"{BASE}{path}", headers=HEADERS, timeout=60, **kwargs)
resp.raise_for_status()
return resp.json()
def wait_for_job(scan_id, timeout_s=600):
cursor = 0
deadline = time.time() + timeout_s
while time.time() < deadline:
job = call("GET", f"/discovery/status/{scan_id}", params={"cursor": cursor})
if "status" not in job:
raise RuntimeError(job.get("error", "unknown job"))
for line in job["logs"]:
print(line)
cursor = job["next_cursor"]
if job["status"] == "COMPLETED":
return job["results"]
if job["status"] == "FAILED":
raise RuntimeError(job["error"])
time.sleep(2)
raise TimeoutError(f"job {scan_id} did not finish in {timeout_s}s")
# 1. Discover
started = call("POST", "/integrations/discover", json={
"hosts": ["10.0.1.15"],
"ports": [5432],
"methods": ["port_scanning"],
})
wait_for_job(started["scan_id"])
# 2. Find the discovered row (the job result has no integer id)
rows = call("GET", "/integrations/discovered", params={
"provider_name": "postgresql",
"configuration_state": "discovered",
})["discovered"]
discovered_id = rows[0]["id"]
# 3. Configure
config = call("POST", f"/integrations/discovered/{discovered_id}/configure", json={
"credentials": {"username": "mmune_reader", "password": "<DB_PASSWORD>", "database": "orders"},
})
# 4. Test (a failed login comes back as success=false, not an HTTP error)
test = call("POST", f"/integrations/discovered/{discovered_id}/test",
json={"config_id": config["configuration_id"]})
if not test["test_result"]["success"]:
raise RuntimeError(test["test_result"]["error"])
# 5. Register (403 means a license cap or block, see detail.code)
registered = call("POST", f"/integrations/discovered/{discovered_id}/register",
params={"config_id": config["configuration_id"]})
integration_id = registered["integration_id"]
# 6. Introspect and read the tables
job = call("POST", f"/integrations/discovered/{discovered_id}/introspect")
wait_for_job(job["scan_id"])
tables = call("GET", f"/integrations/{integration_id}/tables", params={"limit": 50})
print(tables["total"], "tables introspected for", integration_id)TypeScript (fetch)
typescript
const BASE = "https://mmune.example.com/api/v1";
const HEADERS = {
Authorization: "Bearer <TOKEN>",
"Content-Type": "application/json",
};
async function call<T = any>(
method: string,
path: string,
options: { params?: Record<string, string | number>; body?: unknown } = {},
): Promise<T> {
const query = options.params
? "?" + new URLSearchParams(Object.entries(options.params).map(([k, v]) => [k, String(v)]))
: "";
const resp = await fetch(`${BASE}${path}${query}`, {
method,
headers: HEADERS,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
});
if (!resp.ok) {
throw new Error(`${method} ${path} failed with ${resp.status}: ${await resp.text()}`);
}
return resp.json() as Promise<T>;
}
async function waitForJob(scanId: string, timeoutMs = 600_000): Promise<any> {
let cursor = 0;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const job = await call("GET", `/discovery/status/${scanId}`, { params: { cursor } });
if (!("status" in job)) throw new Error(job.error ?? "unknown job");
job.logs.forEach((line: string) => console.log(line));
cursor = job.next_cursor;
if (job.status === "COMPLETED") return job.results;
if (job.status === "FAILED") throw new Error(job.error);
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error(`job ${scanId} did not finish in time`);
}
// 1. Discover
const started = await call("POST", "/integrations/discover", {
body: { hosts: ["10.0.1.15"], ports: [5432], methods: ["port_scanning"] },
});
await waitForJob(started.scan_id);
// 2. Find the discovered row
const listed = await call("GET", "/integrations/discovered", {
params: { provider_name: "postgresql", configuration_state: "discovered" },
});
const discoveredId: number = listed.discovered[0].id;
// 3. Configure
const config = await call("POST", `/integrations/discovered/${discoveredId}/configure`, {
body: { credentials: { username: "mmune_reader", password: "<DB_PASSWORD>", database: "orders" } },
});
// 4. Test
const test = await call("POST", `/integrations/discovered/${discoveredId}/test`, {
body: { config_id: config.configuration_id },
});
if (!test.test_result.success) throw new Error(test.test_result.error);
// 5. Register
const registered = await call("POST", `/integrations/discovered/${discoveredId}/register`, {
params: { config_id: config.configuration_id },
});
// 6. Introspect and read the tables
const job = await call("POST", `/integrations/discovered/${discoveredId}/introspect`);
await waitForJob(job.scan_id);
const tables = await call("GET", `/integrations/${registered.integration_id}/tables`, {
params: { limit: 50 },
});
console.log(tables.total, "tables introspected");Workflow 2: start a schema scan and poll scan status
This uses POST /api/v1/discovery/scan and GET /api/v1/discovery/status/{scan_id}. The same polling code works for the integration jobs in workflow 1.
Python (requests)
python
import time
import requests
BASE = "https://mmune.example.com/api/v1"
HEADERS = {"Authorization": "Bearer <TOKEN>"}
resp = requests.post(
f"{BASE}/discovery/scan",
headers=HEADERS,
json={"target": "10.0.1.15,10.0.1.16", "ports": [5432, 1433]},
timeout=30,
)
resp.raise_for_status()
scan_id = resp.json()["scan_id"]
cursor = 0
while True:
status = requests.get(
f"{BASE}/discovery/status/{scan_id}",
headers=HEADERS,
params={"cursor": cursor},
timeout=30,
).json()
if "status" not in status: # unknown id returns 200 with {"error": ...}
raise RuntimeError(status.get("error", "scan not found"))
for line in status["logs"]:
print(line)
cursor = status["next_cursor"]
if status["status"] == "COMPLETED":
print("found", status["results"]["count"], "database services")
break
if status["status"] == "FAILED":
raise RuntimeError(status["error"])
time.sleep(2)TypeScript (fetch)
typescript
const BASE = "https://mmune.example.com/api/v1";
const HEADERS = {
Authorization: "Bearer <TOKEN>",
"Content-Type": "application/json",
};
const startResp = await fetch(`${BASE}/discovery/scan`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ target: "10.0.1.15,10.0.1.16", ports: [5432, 1433] }),
});
if (!startResp.ok) throw new Error(`scan start failed: ${startResp.status}`);
const { scan_id } = await startResp.json();
let cursor = 0;
for (;;) {
const resp = await fetch(`${BASE}/discovery/status/${scan_id}?cursor=${cursor}`, {
headers: HEADERS,
});
const status = await resp.json();
if (!("status" in status)) throw new Error(status.error ?? "scan not found");
status.logs.forEach((line: string) => console.log(line));
cursor = status.next_cursor;
if (status.status === "COMPLETED") {
console.log("found", status.results.count, "database services");
break;
}
if (status.status === "FAILED") throw new Error(status.error);
await new Promise((r) => setTimeout(r, 2000));
}Workflow 3: send a probe heartbeat with MMUNE_PROBE_KEY
Set MMUNE_PROBE_KEY on the backend and give the same value to each probe. The probe sends it in the X-Probe-Key header. No JWT is involved. Send a heartbeat on a regular interval so last_seen_at stays current. mmune does not require a particular interval.
Python (requests)
python
import os
import socket
import requests
BASE = "https://mmune.example.com/api/v1"
PROBE_KEY = os.environ["MMUNE_PROBE_KEY"]
resp = requests.post(
f"{BASE}/probes/heartbeat",
headers={"X-Probe-Key": PROBE_KEY},
json={
"node_id": "probe-dmz-01",
"hostname": socket.gethostname(),
"capabilities": {"discovery": True},
},
timeout=15,
)
if resp.status_code == 503:
raise RuntimeError("backend has no MMUNE_PROBE_KEY set")
if resp.status_code == 401:
raise RuntimeError("probe key rejected")
resp.raise_for_status()
print(resp.json()) # {"status": "ok"}TypeScript (fetch)
typescript
import { hostname } from "node:os";
const BASE = "https://mmune.example.com/api/v1";
const probeKey = process.env.MMUNE_PROBE_KEY;
if (!probeKey) throw new Error("MMUNE_PROBE_KEY is not set");
const resp = await fetch(`${BASE}/probes/heartbeat`, {
method: "POST",
headers: { "X-Probe-Key": probeKey, "Content-Type": "application/json" },
body: JSON.stringify({
node_id: "probe-dmz-01",
hostname: hostname(),
capabilities: { discovery: true },
}),
});
if (resp.status === 503) throw new Error("backend has no MMUNE_PROBE_KEY set");
if (resp.status === 401) throw new Error("probe key rejected");
if (!resp.ok) throw new Error(`heartbeat failed: ${resp.status}`);
console.log(await resp.json()); // { status: "ok" }Related pages
Authentication, error format, and pagination conventions: API overview. Webhook registration and events: webhooks and events.