Skip to content

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:

GroupPath 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.

LabelMeaning
readBearer JWT with read permission.
writeBearer JWT with write permission or admin role.
adminBearer JWT with the admin role.
agent or writeNo 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-KeyNo JWT is required. The X-Probe-Key header must match MMUNE_PROBE_KEY.

Endpoint index ​

Integrations (/api/v1/integrations) ​

MethodPathPurpose
GET/api/v1/integrations/supportedList the providers this build can detect and introspect.
GET/api/v1/integrations/coverageReport how many discovered endpoints are mapped, unidentified, or blocked.
POST/api/v1/integrations/discoverStart a background discovery run.
GET/api/v1/integrations/discoveredList 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}/configureSave credentials for a discovered integration.
POST/api/v1/integrations/discovered/{discovered_id}/testTest the connection using a saved or supplied configuration.
POST/api/v1/integrations/discovered/{discovered_id}/registerRegister the integration so it is monitored.
POST/api/v1/integrations/discovered/{discovered_id}/introspectStart a background schema introspection for one integration.
POST/api/v1/integrations/introspect-allStart a background introspection of every registered database.
GET/api/v1/integrations/registeredList registered integrations.
GET/api/v1/integrations/registered/{integration_id}Get one registered integration.
GET/api/v1/integrations/{integration_id}/tablesPage 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) ​

MethodPathPurpose
GET/api/v1/discovery/schemasList discovered database schemas.
POST/api/v1/discovery/scanStart 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-findingsList ranked, non-dismissed findings about the estate.
POST/api/v1/discovery/first-findings/{finding_id}/dismissDismiss one finding.

Discovery agents (/api/v1/discovery-agents) ​

MethodPathPurpose
POST/api/v1/discovery-agents/registerRegister an agent and mint its token.
GET/api/v1/discovery-agentsList 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}/healthGet agent health.
POST/api/v1/discovery-agents/{agent_id}/heartbeatRecord an agent heartbeat.
GET/api/v1/discovery-agents/{agent_id}/configFetch the agent configuration.
PUT/api/v1/discovery-agents/{agent_id}/configReplace the agent configuration.
POST/api/v1/discovery-agents/{agent_id}/discoveriesSubmit discovered integrations, optionally with schemas.
POST/api/v1/discovery-agents/{agent_id}/kafka-flow-samplesSubmit Kafka consumer lag readings.

Projects (/api/v1/projects) ​

MethodPathPurpose
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}/startMove a project to the discovery status.

Probes (/api/v1/probes) ​

MethodPathPurpose
POST/api/v1/probes/heartbeatRegister or refresh a probe node.
GET/api/v1/probesList 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:

FieldTypeDescription
providersarrayOne entry per provider, sorted by provider_id.
providers[].provider_idstringCanonical id, for example postgresql.
providers[].display_namestringHuman name.
providers[].categorystringOne of oltp, olap, message_broker, cache, search, orchestrator, saas, other, infrastructure.
providers[].tierstringCoverage tier T1 to T4, computed at request time.
providers[].capabilitiesstring[]Sorted subset of fingerprint, credential_defaults, connection_probe, introspect, re_introspect, value_drift, containment.
providers[].portsinteger[]Default ports used for fingerprinting.
providers[].driver_installedbooleanWhether the provider's driver is available in this deployment.
providers[].reachabilitystringestate_local or internet.
providers[].estate_cost_noteobjectintrospect_queries_per_table (integer), row_count_strategy (exact_count, catalog_estimate, none), billed_by_vendor (boolean).
countintegerNumber 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:

FieldTypeDescription
endpointsarrayOne entry per discovered row, ordered by id.
endpoints[].idintegerDiscovered id.
endpoints[].integration_idstringIntegration id.
endpoints[].host, endpoints[].portstring or null, integer or nullNetwork location.
endpoints[].providerstringProvider name recorded at discovery.
endpoints[].tierstring or nullComputed tier, null when the provider has no spec.
endpoints[].statestring or nullPersisted coverage state, for example mapped, needs_connector, needs_driver, needs_credentials, unidentified.
endpoints[].reason, endpoints[].detail, endpoints[].atstring or nullReason code, free text, and timestamp of the state.
summary.detectedintegerAll discovered rows.
summary.mapped, needs_connector, needs_driver, needs_credentials, unidentifiedintegerRows in each named state.
summary.coverage_scorenumber or null1 - 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:

NameTypeRequiredDefaultDescription
hostsstring[]nonullIP addresses or CIDR ranges to probe. When empty, port scanning targets 127.0.0.1.
portsinteger[]nonullPorts to probe. When empty, the known service ports are used. The MMUNE_SCAN_PORT_RANGE environment variable always adds to this set.
api_endpointsstring[]nonullKnown API base URLs for API discovery.
openapi_specsstring[]nonullOpenAPI spec URLs.
graphql_endpointsstring[]nonullGraphQL endpoint URLs.
cloud_providersstring[]nonullCloud providers to enumerate.
cloud_regionsstring[]nonullCloud regions to enumerate.
config_pathsstring[]nonullConfig file paths to analyze.
methodsstring[]no["port_scanning", "api_discovery"]Any of port_scanning, api_discovery, config_analysis, cloud_api, manual, import, agent_push.
project_idstringnonullProject to associate the results with.

An unrecognized value in methods causes the request to fail.

Response 200:

FieldTypeDescription
messagestringDiscovery started.
scan_idstringJob id to poll.
statusstringRUNNING.

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:

NameTypeRequiredDefaultDescription
project_idstringnonullOnly rows for this project.
integration_typestringnonullOne of database, saas_platform, api, file_system, message_queue, cloud_service, erp_system, legacy_system, custom_app, data_warehouse, streaming, microservice.
integration_patternstringnonullOne of rest_api, sql_protocol, file_based, message_based, graphql_api, soap_api, streaming, batch, webhook.
provider_namestringnonullProvider id such as postgresql.
configuration_statestringnonulldiscovered, 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:

NameTypeRequiredDefaultDescription
credentialsobjectno{}Connection credentials and options.
auth_configobject or nullnonullAuthentication block, mainly for REST APIs. Either nested ({"method": "basic_auth", "config": {...}}) or flat ({"username": "...", "password": "..."}).

Response 200:

FieldTypeDescription
messagestringIntegration configured successfully.
configuration_idintegerId to pass as config_id to test or register.
discovered_idintegerEcho 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:

NameTypeRequiredDefaultDescription
config_idinteger or nullnonullConfiguration to test. The latest one is used when omitted.
config_overrideobject or nullnonullInline 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:

NameTypeRequiredDefaultDescription
config_idintegernonullConfiguration to register with. The latest one is used when omitted.

Response 200:

FieldTypeDescription
messagestringIntegration registered successfully.
integration_idstringId used in /registered/... and lineage.
provider_namestringProvider id.
capabilitiesstring[]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:

NameTypeRequiredDefaultDescription
limitintegerno50Page size, 1 to 200.
offsetintegerno0Rows to skip, 0 or more.
qstringnonullCase-insensitive substring match on the table node key, up to 200 characters.

Response 200:

FieldTypeDescription
integration_id, providerstringThe integration and its provider name.
databasesarrayPer database: database, node_key, tables_introspected.
tables_introspected_totalinteger or nullSum of table counts across databases, null if none recorded.
table_nodes_in_lineageintegerTable nodes for this integration, ignoring q.
total, limit, offsetintegerCount matching q and the paging values used.
tablesarrayPer table: node_key, database, schema, table, column_count, row_count_source (unknown when absent), watch.
watch_windowobjectprovider_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:

NameTypeRequiredDefaultDescription
codestringyesnoneAuthorization code.
discovered_idintegeryesnoneDiscovered integration to configure.
client_idstringyesnoneOAuth client id.
client_secretstringyesnoneOAuth client secret.
redirect_uristringnonullRedirect 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:

FieldTypeDescription
schemasarraySchema 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.
countintegerNumber of entries.
sourcestringintrospection, 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:

NameTypeRequiredDefaultDescription
targetstringno""A host, or a comma-separated list of hosts. With an empty value the scan covers localhost.
portsinteger[]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:

NameTypeRequiredDefaultDescription
cursorintegerno0Log offset, 0 or more.

Response 200:

FieldTypeDescription
statusstringRUNNING, COMPLETED, or FAILED.
logsstring[]Log lines from cursor onward.
next_cursorintegerValue to send as cursor on the next poll.
resultsanyJob result once completed, otherwise null.
created_at, updated_atstringISO 8601 timestamps.
errorstring or nullFailure 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:

NameTypeRequiredDefaultDescription
limitintegerno50Maximum findings, 1 to 500.

Response 200:

FieldTypeDescription
findingsarrayEach has id, kind, integration_id, object_key, statement, numbers (object), severity, blast_radius, computed_at, dismissed.
countintegerFindings returned.
estate_summaryobjectintegrations_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:

NameTypeRequiredDefaultDescription
agent_idstringyesnoneUnique agent identifier.
namestringnoagent_idDisplay name.
versionstringnounknownAgent version.
platformstringnounknownFor example linux, windows, docker.
capabilitiesstring[]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:

NameTypeRequiredDefaultDescription
statusstringnonullOnly 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:

NameTypeRequiredDefaultDescription
metricsobject or nullnonullFree-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:

NameTypeRequiredDefaultDescription
configobjectyesnoneComplete 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:

NameTypeRequiredDefaultDescription
discoveriesarrayyesnoneItems described next.
discoveries[].integration_typestringyesnoneProvider 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[].namestringyesnoneIntegration name.
discoveries[].confidence_scorenumberyesnone0.0 to 1.0.
discoveries[].discovery_methodstringyesnoneFor example port_scan, config_file.
discoveries[].endpointstringnonullhost:port or URL.
discoveries[].hoststringnonullHost address.
discoveries[].portintegernonullPort number.
discoveries[].metadataobjectno{}Extra details.
discoveries[].agent_schemaobjectnonullSchema snapshot, described next.
agent_schema.databasestringnodefaultDatabase name.
agent_schema.tablesarrayno[]Tables.
tables[].tablestringyesnoneTable name.
tables[].db_schemastringnodatabase nameSchema name.
tables[].row_countintegerno-1Row count, -1 when unknown.
tables[].columnsarrayno[]Columns.
columns[].namestringyesnoneColumn name.
columns[].typestringnoUNKNOWNColumn type.
columns[].nullablebooleannotrueNullability.
columns[].piibooleannofalseWhether the agent flagged the column as PII.

Response 200:

FieldTypeDescription
statusstringok.
messagestringProcessed {n} discoveries.
storedintegerNew rows created.
duplicatesintegerItems that matched an existing row.
errorsintegerItems that failed and were skipped.
lineage_tables_upsertedintegerTable 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:

NameTypeRequiredDefaultDescription
hoststringyesnoneKafka host. It is matched against previously discovered integrations by host and port.
portintegeryesnoneKafka port.
lagsarrayno[]Readings.
lags[].topicstringyesnoneTopic name.
lags[].partitionintegeryesnonePartition number.
lags[].groupstringyesnoneConsumer group.
lags[].high_water_markintegeryesnoneLatest offset in the partition.
lags[].committed_offsetintegeryesnoneOffset the group has committed.
lags[].has_consumerbooleannotrueWhether the group has an active consumer.

Response 200 has one of three shapes, keyed by status:

statusOther fieldsMeaning
skippedreason: "integration_not_discovered"No discovered integration matches host and port yet. Not an error; push again after the discovery push lands.
processed_no_sessionfindings_countFindings were computed, but no monitoring session is attached, so no alerts were raised.
processedfindings_count, dispatch_countFindings 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:

ObjectFields
Database configtype (string, required), host, port (integer), database, username, password, connection_string (all optional strings except port), options (object, default {}).
Migration configbatch_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 configFive 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 configsource (database config, required), target (database config, required), migration (migration config), agents (agents config).
Projectid, name, description, status, config, metrics, progress, created_at, updated_at, created_by (the creator's email), tags.
metricstables_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.
progressdiscovery, 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:

NameTypeRequiredDefaultDescription
pageintegerno1Page number, minimum 1.
page_sizeintegerno10Items per page, clamped to 1 to 100.
statusstringnonullExact status match.
searchstringnonullCase-insensitive match on name or description.
sort_bystringnonullname, created_at, or updated_at. Other values leave the order unchanged.
sort_orderstringnoascasc 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:

NameTypeRequiredDefaultDescription
namestringyesnone3 to 100 characters.
descriptionstringyesnone10 to 1000 characters.
configproject configyesnoneOnly source and target are required inside it.
tagsstring[]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:

NameTypeRequiredDefaultDescription
namestringnonull3 to 100 characters.
descriptionstringnonull10 to 1000 characters.
configproject confignonullFull replacement config.
tagsstring[]nonullFull replacement tag list.
statusstringnonullFree-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:

NameRequiredDescription
X-Probe-KeyyesMust equal the backend's MMUNE_PROBE_KEY.

Body:

NameTypeRequiredDefaultDescription
node_idstringyesnoneUnique probe id. The record is keyed on it.
hostnamestringnonullProbe host name.
capabilitiesobjectno{}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:

NameTypeRequiredDefaultDescription
pageintegerno1Page number.
page_sizeintegerno10Items 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_idAliasesNameDefault portsIntrospects schema
postgresqlnonePostgreSQL5432yes
mysqlnoneMySQL3306yes
mariadbnoneMariaDBnoneyes
sqlserversql_server, mssqlSQL Server1433yes
oraclenoneOracle1521yes
mongodbnoneMongoDB27017yes
redisredis-oss, valkeyRedis6379yes
cassandrascyllaApache Cassandra9042yes
elasticsearchelastic, opensearchElasticsearch9200yes
kafkanoneApache Kafka9092yes
rabbitmqnoneRabbitMQ5672yes
airflowapache_airflow, apache-airflowApache Airflownoneyes
snowflakesfSnowflakenoneyes
sap_hanasapSAP HANA39013yes
sap_rfcnoneSAP RFCnoneyes
tibcononeTIBCOnoneyes
db2_zosdb2IBM DB2 z/OS50000no
teradatanoneTeradata1025no
activemqnoneApache ActiveMQ61616no
solrnoneApache Solrnoneno
httpnoneHTTP80, 443, 8080no
sshnoneSSH22no
smtpnoneSMTP25no
ftp, ftp_or_smtp, imap, pop3, vnc, tlsnoneFTP, FTP or SMTP (ambiguous banner), IMAP, POP3, VNC, TLS (certificate-only)noneno
grafana, splunknoneGrafana, Splunknoneno

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.

KeyTypeDefaultDescription
usernamestringnullLogin. user is also read by the introspector.
passwordstringnullPassword.
databasestringprovider defaultDatabase name. db is accepted by the introspector. Defaults when omitted: postgres for PostgreSQL, master for SQL Server, empty for MySQL and Oracle.
connection_timeoutinteger30Seconds.
query_timeoutinteger300Seconds.
ssl_enabledbooleanfalseUse TLS.
ssl_configobject{}TLS options.
connection_pool_sizeinteger10Pool size.
max_connectionsinteger50Connection ceiling.
additional_paramsobject{}Provider-specific extras. Any key not listed here is moved into additional_params.

Provider-specific keys:

ProviderKeyDescription
oracleservice_nameService name. Falls back to database, then MMUNE_ORACLE_SERVICE, then FREEPDB1.
oracleschemasComma-separated string or list that bounds catalog reads. Falls back to MMUNE_ORACLE_SCHEMAS.
sap_hanaschemas or schemaSchemas to introspect. Falls back to MMUNE_HANA_SCHEMAS, then database.
sap_hanaencryptTLS on the connection. Default true. MMUNE_HANA_ENCRYPT overrides.
sap_hanassl_validateValidate the server certificate. Defaults to true in production mode. MMUNE_HANA_SSL_VALIDATE overrides.
sap_hanaca_certPEM trust store for a private CA. MMUNE_HANA_CA_CERT supplies it from the environment.
sap_hanatenantMulti-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.

KeyDescription
rfc_hostApplication server host. Falls back to MMUNE_SAP_RFC_HOST.
rfc_sysnrSystem number. Falls back to MMUNE_SAP_RFC_SYSNR.
rfc_clientClient. Falls back to MMUNE_SAP_RFC_CLIENT, then 000.
rfc_user, rfc_passwordRFC login. Fall back to the integration's username and password.
rfc_ashost, rfc_mshost, rfc_group, rfc_sysidOptional 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.

KeyWhereDefaultDescription
auth_config.methodauth_configinferredOne of none, basic_auth, api_key, oauth2, jwt, bearer_token. Inferred from the keys present when omitted.
api_key, header_namecredentialsheader X-API-KeyAPI key authentication.
username, passwordcredentials or auth_confignoneBasic authentication.
access_token (or token, bearer_token in auth_config)eithernoneBearer token.
client_id, client_secret, token_url, grant_typecredentialstoken URL {base_url}/oauth/token, grant client_credentialsOAuth 2.0 client credentials.
default_headerscredentials{}Extra request headers.
timeoutcredentials30Seconds.
max_retriescredentials3Retry count.
retry_delaycredentials1.0Seconds between retries.
enable_pagination, pagination_configcredentialstrue, {}Pagination handling.
rate_limitcredentialsnoneRate 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.

KeyDefaultDescription
username (or user)<ssh-user>SSH user.
ssh_key_pathMMUNE_TIBCO_SSH_KEY_PATHPrivate key path. Required; without it introspection is skipped with a credentials_missing reason.
ssh_port22SSH port.
ssh_key_passphrasenoneKey passphrase.
known_hosts_policytrueReject unknown host keys.
known_hosts_pathMMUNE_TIBCO_KNOWN_HOSTSKnown hosts file.
enabled_productsallProducts to harvest.
log_confignoneElastic log source: endpoint, index_pattern (default tibco-logs-*), username, password, api_key, verify_certs (default true), ca_cert_path.
ems_adminnoneLive 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" }

Authentication, error format, and pagination conventions: API overview. Webhook registration and events: webhooks and events.