Skip to content

Mapping, lineage, semantic, duplicates, recommendations, reconciliation and analysis API ​

What this page covers ​

This page documents the HTTP endpoints for mapping, mapping feedback, lineage, lineage sync, semantic, duplicates, recommendations, reconciliation and analysis. For each one you get the method, full path, purpose, required permission, request fields, response fields, status codes and a curl example. Four worked examples at the end show the same calls in Python (requests) and TypeScript (fetch).

Prerequisites: a running mmune backend, a Bearer JWT for a user with at least the read permission (write for most mutating calls), and the conventions on API overview. Authentication, error body shapes and pagination behavior are defined there and are not repeated here. All examples use https://mmune.example.com as the host and <TOKEN> as the JWT.

Endpoint summary ​

The Needs column shows the permission each call requires, as described under Permissions on this page.

MethodPathPurposeNeeds
POST/api/v1/mapping/mapMap one vendor record to the target model and persist the resultwrite
POST/api/v1/mapping/suggestAsk the mapping agent for the best target column for one source fieldread
GET/api/v1/mapping/savedList stored mappings, newest firstread
GET/api/v1/mapping/saved/{filename}Fetch one stored mappingread
POST/api/v1/mapping/feedbackRecord operator feedback on a mappingwrite
GET/api/v1/mapping/feedbackList recorded feedback rowsread
GET/api/v1/mapping/feedback/metricsAccuracy metrics and volume counts over feedbackread
POST/api/v1/lineage/edgeCreate or update one lineage edge (and its endpoint nodes)write
GET/api/v1/lineage/graphReturn the whole lineage graphread
GET/api/v1/lineage/impact/{node_key}Downstream blast radius of one noderead
POST/api/v1/lineage/syncStart a background lineage sync, returns a job idwrite
POST/api/v1/lineage/sync/nowRun a lineage sync and wait for the resultwrite
POST/api/v1/semantic/extract-intentExtract business intent for one field namewrite
GET/api/v1/semantic/mappings-by-intent/{intent}Semantic mappings that share one business intentread
POST/api/v1/semantic/find-semantic-matchFind an existing mapping that matches a source field to target fieldsread
GET/api/v1/semantic/mapping-history/{mapping_id}Version history of one semantic mappingread
GET/api/v1/semantic/intent-registry/statsCounts from the intent registryread
GET/api/v1/semantic/intent-registry/signaturesSignatures held in the intent registryread
GET/api/v1/semantic/statsExplorer stats computed from persisted semantic mappingsread
GET/api/v1/semantic/signaturesExplorer signature rows computed from persisted mappingsread
GET/api/v1/semantic/signatures/{signature_hash}/matchesSystems that contain fields for one signatureread
GET/api/v1/duplicates/configRead the duplicate handling moderead
PUT/api/v1/duplicates/configSet the duplicate handling modewrite
POST/api/v1/duplicates/validate-before-writeGate a record before it is written to a targetwrite
GET/api/v1/duplicates/groupsList known duplicate groupsread
POST/api/v1/duplicates/detectDetect duplicate groups in a batch of recordswrite
GET/api/v1/duplicates/{record_id}Duplicate group that contains one recordread
POST/api/v1/duplicates/resolveRecord a resolution for a duplicate groupwrite
GET/api/v1/duplicates/resolutions/historyResolution historyread
POST/api/v1/duplicates/checkCheck one record against existing recordswrite
GET/api/v1/recommendationsList estate advisor recommendationsread
POST/api/v1/recommendations/scanRun the advisor scanwrite
PATCH/api/v1/recommendations/{recommendation_id}Dismiss, resolve or reactivate a recommendationwrite
GET/api/v1/reconciliation/pairingsList reconciliation pairingsread
GET/api/v1/reconciliation/pairings/{pairing_id}One pairing with per-partition stateread
POST/api/v1/reconciliation/pairingsDeclare a pairing in proposed statuswrite
PATCH/api/v1/reconciliation/pairings/{pairing_id}Edit a proposed or paused pairingwrite
POST/api/v1/reconciliation/pairings/{pairing_id}/activateActivate a pairingwrite
POST/api/v1/reconciliation/pairings/{pairing_id}/pausePause an active pairingwrite
POST/api/v1/reconciliation/pairings/{pairing_id}/rejectReject a proposed or paused pairingwrite
GET/api/v1/reconciliation/findingsList reconciliation findingsread
POST/api/v1/analysis/chatAsk a question about the deploymentread
POST/api/v1/analysis/chat/streamSame as chat, streamed as server-sent eventsread

Permissions on this page ​

Every request needs a valid Bearer JWT with the read permission. Any method other than GET, HEAD and OPTIONS additionally needs write (or the admin role). The only mutating calls on this page that need just read are POST /api/v1/mapping/suggest, POST /api/v1/semantic/find-semantic-match, POST /api/v1/analysis/chat and POST /api/v1/analysis/chat/stream. The Needs column and the "Needs:" lines below give the requirement for each call.

Concepts you need before calling these endpoints ​

Lineage node keys and node types ​

A lineage node is identified by a unique string, node_key. The graph returns it as id. Keys written by introspection and by the discovery agents are built by dotted concatenation from the integration id, which is an opaque string you should not parse. Nodes written by lineage sync use a different pattern, described below the table.

Node typeKey patternMeaning
source{integration_id}A registered integration. The key is the integration id itself.
endpoint{integration_id}A detected but not yet registered integration. Registering it later upgrades the same key to source.
database{integration_id}.{database}One database on a source.
table{integration_id}.{database}.{table}One table. The schema is stored in attributes, not in the key.
rfc_destination{integration_id}.rfc.{name}SAP RFC destination.
job{integration_id}.job.{name}A background job or Airflow DAG ({integration_id}.job.{dag_id}).
idoc_partner{integration_id}.idoc.{partner}SAP IDoc partner profile.
abap_program, sap_transport{integration_id}.prog.{name}, {integration_id}.transport.{trkorr}SAP program and transport nodes.
external_system{integration_id}.ext.{name}A remote system referenced by a destination or partner.
tibco_service, tibco_process, tibco_queue, tibco_topic, tibco_endpoint{integration_id}.svc.{name}, .proc.{name}, .queue.{name}, .topic.{name}, .endpoint.{name}TIBCO topology nodes.

Lineage sync writes only table nodes, and it does not prefix them with the integration id. Their keys are {db_name}.{schema}.{table} (MySQL uses {db_name}.{table}), where db_name is the integration's database metadata and falls back to {host}_{port} when that is absent. The integration id is stored in the node's integration_id attribute instead, and columns on these nodes is an integer count rather than a list. If you filter the graph by key prefix, also match on metadata.integration_id to catch sync-written nodes.

Nodes created through POST /api/v1/lineage/edge are typed table. A node that already exists keeps its type.

Edges have a source, a target and a label (the stored edge_type). Types you will see include contains (source to database to table), foreign_key, inferred_cross_source, connects_to, declares, defines, exchanges_with, routes_via, runs, schedules, sends_to and mapping (the default for POST /api/v1/lineage/edge). An inferred_cross_source edge links two tables in different integrations whose identifier columns share hashed values. Its metadata carries shared_column, jaccard, confidence, samples_a, samples_b, intersection and detected_by.

Both the graph and the impact walk add a quarantined boolean to nodes. It reflects containment state only and never changes a health status.

Health statuses ​

None of the endpoints on this page returns an integration or node health field. Lineage nodes and reconciliation findings carry no health field. You still meet the health vocabulary when you correlate these responses with the endpoints in Health and monitoring. Integrations report healthy, degraded or broken. Treat broken as the worst integration state and degraded as partially working.

One attribution rule matters here. A reconciliation finding is recorded against the receiving (B side) integration, so integration_id on a finding is the B system, not the system that is missing data.

Row-count provenance ​

Table nodes in the lineage graph carry metadata.row_count and metadata.row_count_source. The count and its source are recorded once, at introspection, and are per table, so one integration can mix sources.

row_count_sourceMeaning
exactA real count of the table.
catalog_estimateThe database's own statistic (for example pg_class.reltuples or information_schema.TABLES.TABLE_ROWS). It can drift between statistics refreshes.
unknownNo usable count, or a node written before provenance was recorded. A missing field must be read as unknown, never as exact.

row_count of -1 means unknown. Catalog estimates are the default. Exact counts are an opt-in per provider through MMUNE_POSTGRES_ROW_COUNT_EXACT, MMUNE_MYSQL_ROW_COUNT_EXACT and MMUNE_MONGODB_ROW_COUNT_EXACT. If you show a count to a person, show an exact one with digit grouping and anything else as approximate.

Semantic mapping model ​

A semantic mapping is a stored record that links a source field to a target field and tags it with a business intent. Mappings are created automatically during introspection, one per unique column name in each integration, and by the healing workflow when a remap is applied. No endpoint on this page creates a semantic mapping directly. The endpoints below read them, search them and extend the intent registry.

FieldTypeMeaning
mapping_idstring (UUID)Primary key.
source_vendorstringIdentifier of the source system, normally an integration id.
source_schema, source_table, source_fieldstring or null, null, stringSource location.
target_model, target_schema, target_table, target_fieldstring, null, null, stringTarget location.
business_intentstring or nullConcept name such as Customer Name. This is what mappings-by-intent matches, exactly.
business_intent_categorystring or nullOne of customer, order, product, payment, address, contact, identifier, date_time, amount, status, code, description, metadata, other.
semantic_signaturestring or nullStable signature of the intent.
conceptstring or nullConcept label, used when business_intent is empty.
mapping_typestringdirect, transform, split, merge and so on.
transformation_type, transformation_expression, transformation_parametersstring, string, object (all nullable)Transformation detail.
confidence_scorenumberRequired, defaults to 0.0.
similarity_score, semantic_equivalence_scorenumber or nullOptional scores.
organization_idstringSingle-tenant deployment id. It is derived on the server and never supplied by the caller.
field_description, sample_values, notesstring, array, string (nullable)Context.
created_at, updated_at, created_byISO 8601 string, ISO 8601 string, stringAudit fields.

A row is unique on (source_vendor, source_field, target_model, target_field, organization_id). Every change creates a version row (version_id, mapping_id, version_number, schema_snapshot, changed_fields, semantic_equivalence_score, change_type, change_reason, changed_by, changed_at) that mapping-history returns newest first.

Reconciliation model ​

A pairing declares that table A in one integration and table B in another should agree on a measure (an amount) per partition (for example per bill-run date). mmune never activates a pairing by itself. A person calls activate. Once active, a background runner checks the pairing on its own cadence (cadence_seconds, default 900). There is no endpoint to run a check on demand, so you create the pairing, activate it, then poll the pairing and the findings.

Status transitions are enforced and anything else returns 409 illegal_transition.

FromAllowed next status
proposedactive, rejected
activepaused
pausedactive, rejected
rejectednone

Amounts are integers in minor units (cents for a two-decimal currency), never floats. Each side declares a measure scale (a_measure_scale, b_measure_scale, an integer 0 to 6), and the database multiplies by 10 to the power of the scale and rounds. Use 2 for a numeric(12,2) dollar column and 0 for a column that already stores cents. minor_unit_exponent only controls how the *_display strings are formatted.

Every check compares per-partition aggregates first: a key count and an amount sum per partition, computed inside each source database. For a partition that has stayed in disagreement past its settle window on two consecutive checks, mmune may look deeper at that partition. The finding's comparison field tells you which method was used: aggregate, key_level or fingerprint.

What leaves your source databases: routine checks return only aggregates. A deeper comparison reads key and amount values for the partition under investigation only. For larger partitions, each source returns a keyed hash of every key instead of the key itself, so the identifiers stay in your database, and only the hashes and amounts are compared. Compared values are used transiently and are never stored, logged or returned. Findings never include record-level keys or amounts. They do include the literal partition value and a read-only lookup_sql statement per side so an operator can list the partition's rows in their own tooling.


Mapping router ​

Path prefix: /api/v1/mapping.

POST /api/v1/mapping/map ​

Maps one vendor record onto a target model with the mapping agent, validates the access through the guardian, and persists the result. Needs: write.

Query parameter:

NameTypeRequiredDefaultDescription
target_modelstringnoUniversalCustomerTarget model. UniversalCustomer is the supported target, with the columns firstName, lastName, email and status.

Body (camelCase names are required, the server does not accept snake_case here):

NameTypeRequiredDescription
vendorIdstringyesVendor identifier. Becomes sourceVendor in the result.
vendorNamestringyesDisplay name.
objectTypestringyesKind of record, for example customer.
rawDataobjectyesThe record. Column names are taken from its keys.
fieldMappingsarray or nullnoHints. Each item has vendorField, targetField and optional transformationType (direct, computed, lookup, custom). Default null.
metadata.recordIdstringyesVendor record id.
metadata.recordTypestringnoRecord type.
metadata.lastModifiedISO 8601 datetimeyesLast modified time.
metadata.apiVersionstringnoVendor API version.
metadata.retrievedAtISO 8601 datetimeyesWhen the record was fetched.

Response 200 (MappingResult, camelCase):

FieldTypeDescription
mappingIdstringResult id.
sourceVendorstringEcho of vendorId.
targetModelstringEcho of target_model.
mappedDataobjectTarget column name to source value.
fieldMappingsarrayItems with sourceField, targetField, sourceValue, mappedValue, confidence, transformationType (direct, computed, inferred, custom) and optional aiReasoning.
isValidbooleanTrue when the agent returned no errors.
validationErrors, validationWarningsarrayItems with field, errorType (missing_required, invalid_format, out_of_range, type_mismatch, custom), message, severity (error, warning, info).
aiModel, processingTimestring, numberOptional.
metadataobjectmappedAt, mappedBy, reviewStatus (pending, approved, rejected), reviewedBy, reviewedAt.

The result is stored and can be retrieved later with GET /api/v1/mapping/saved. A later call for the same vendor and target model replaces the stored mapping.

Status codes: 200, 403 (guardian refused access), 422 (body failed validation), 500 (mapping failed).

bash
curl -X POST "https://mmune.example.com/api/v1/mapping/map?target_model=UniversalCustomer" \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "vendorId": "crm-eu",
    "vendorName": "CRM Europe",
    "objectType": "customer",
    "rawData": {"first_name": "Dana", "last_name": "Levi", "email_address": "dana@example.com", "state": "active"},
    "metadata": {"recordId": "c-1001", "lastModified": "2026-10-01T09:00:00Z", "retrievedAt": "2026-10-04T08:00:00Z"}
  }'

POST /api/v1/mapping/suggest ​

Asks the mapping agent which of several target columns best matches one source field. Needs: read.

NameTypeRequiredDescription
source_fieldstringyesSource column name.
target_candidatesarray of stringyesCandidate target column names.

Response 200: an array with zero or one object. The object has target_column (string or null) and reasoning (string). An empty array means no suggestion was available, for example when no AI provider is configured or the reply could not be used. Status codes: 200, 422.

bash
curl -X POST https://mmune.example.com/api/v1/mapping/suggest \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"source_field": "cust_email", "target_candidates": ["email", "phone", "first_name"]}'

GET /api/v1/mapping/saved ​

Lists stored mappings, newest first. Needs: read. No parameters. Response 200: array of strings, each the name of one stored mapping, which you pass to GET /api/v1/mapping/saved/{filename}. Status codes: 200, 500.

bash
curl https://mmune.example.com/api/v1/mapping/saved -H "Authorization: Bearer <TOKEN>"

GET /api/v1/mapping/saved/ ​

Returns one stored MappingResult (same shape as the response of POST /map). Needs: read.

NameInTypeRequiredDescription
filenamepathstringyesA name returned by GET /saved.

Status codes: 200, 404 (mapping not found), 500.

bash
curl https://mmune.example.com/api/v1/mapping/saved/<SAVED_NAME> \
  -H "Authorization: Bearer <TOKEN>"

Mapping feedback router ​

These endpoints are also under /api/v1/mapping. They capture operator judgments about mappings and do not change any mapping.

POST /api/v1/mapping/feedback ​

Stores one feedback row. Needs: write.

NameTypeRequiredDescription
mapping_idstringyesMapping the feedback is about. Minimum length 1.
source_fieldstringyesMinimum length 1.
target_fieldstringyesMinimum length 1.
feedback_typestringyescorrect, incorrect, partial or manual_override (case-insensitive, trimmed).
ai_suggested_fieldstringnoDefaults to target_field when omitted.
user_corrected_fieldstringnoThe field the operator chose instead.
user_commentstringnoFree text.
ai_confidencenumbernoConfidence the AI reported.
ai_reasoningstringnoThe AI's stated reason.
source_table, target_tablestringnoTables for the two fields.
source_vendor, target_modelstringnoContext.
project_idstringnoProject scope.
sentimentstringnoFree text label.
data_typestringnoColumn type.
transformation_appliedstringnoTransformation that was used.
session_idstringnoSession reference.

Response 201: the stored row with id, mapping_id, source_field, target_field, source_table, target_table, ai_suggested_field, ai_confidence, ai_reasoning, feedback_type, user_corrected_field, user_comment, sentiment, source_vendor, target_model, created_at, was_used_for_training and project_id. Status codes: 201, 422 (unknown feedback_type, or body validation), 500 (Failed to record mapping feedback).

bash
curl -X POST https://mmune.example.com/api/v1/mapping/feedback \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"mapping_id": "m-123", "source_field": "cust_email", "target_field": "email", "feedback_type": "correct"}'

GET /api/v1/mapping/feedback ​

Lists feedback rows. Needs: read. Response 200 is a bare array of the row shape above (no envelope).

NameInTypeRequiredDefaultDescription
mapping_idquerystringnononeFilter.
project_idquerystringnononeFilter.
feedback_typequerystringnononeFilter. Validated against the four values above (422 otherwise).
limitqueryintegerno100Range 1 to 500.
bash
curl "https://mmune.example.com/api/v1/mapping/feedback?feedback_type=incorrect&limit=50" \
  -H "Authorization: Bearer <TOKEN>"

GET /api/v1/mapping/feedback/metrics ​

Accuracy metrics plus volume counts. Needs: read.

NameInTypeRequiredDefaultDescription
project_idquerystringnononeRestrict to one project.
daysqueryintegerno30Window for the accuracy block, range 1 to 365.

Response 200:

FieldTypeDescription
accuracyobjecttotal_mappings, correct_predictions, incorrect_predictions, partial_predictions, manual_overrides, average_confidence (2 decimals), accuracy_rate (percent, 2 decimals; a partial counts as half a correct).
volumeobjectmapping_total, mapping_non_correct (incorrect, partial and manual override), mapping_by_type (count per feedback type). Computed over all feedback for the project, not limited by days.
project_id, daysstring or null, integerEcho of the query.
bash
curl "https://mmune.example.com/api/v1/mapping/feedback/metrics?days=90" -H "Authorization: Bearer <TOKEN>"

Lineage router ​

Path prefix: /api/v1/lineage. See Lineage node keys and node types.

POST /api/v1/lineage/edge ​

Creates or updates one edge, and creates both endpoint nodes as table nodes if they do not exist. Needs: write.

NameTypeRequiredDefaultDescription
sourcestringyesnoneSource node_key.
targetstringyesnoneTarget node_key.
edge_typestringnomappingEdge label. Edges are unique on (source, target, edge_type).
metadataobjectnonullAttributes stored on the edge. A repeat call replaces them.

Response 200: {"ok": true}. If the edge could not be written (for example when source is empty), the response is {"ok": false} with status 200, so check the body, not only the status. You cannot set a node type through this endpoint. Status codes: 200, 422.

bash
curl -X POST https://mmune.example.com/api/v1/lineage/edge \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"source": "pg_ab12cd34.sales.orders", "target": "pg_ef56ab78.finance.ledger", "edge_type": "mapping"}'

GET /api/v1/lineage/graph ​

Returns every node and edge. There are no query parameters and no pagination, so filter on the client. Needs: read.

Response 200:

FieldTypeDescription
nodes[].idstringThe node_key.
nodes[].labelstringProvider display name for source and endpoint nodes, otherwise the last dot-separated segment of the key.
nodes[].typestringNode type.
nodes[].provider, .host, .schemastringHoisted from attributes, empty string when absent.
nodes[].metadataobject or nullFull attributes. For tables this includes schema, table, database, row_count, row_count_source, column_count, columns, has_pii and pii_fields. columns is a list on nodes written by introspection and an integer count on nodes written by lineage sync.
nodes[].quarantinedbooleanContainment flag.
edges[].idstring{source}-{target}.
edges[].source, .targetstringNode keys.
edges[].labelstringEdge type.
edges[].metadataobject or nullEdge attributes.
bash
curl https://mmune.example.com/api/v1/lineage/graph -H "Authorization: Bearer <TOKEN>"

GET /api/v1/lineage/impact/ ​

Walks edges forward from a node and returns everything downstream of it. The path parameter accepts dots and slashes. Needs: read.

NameInTypeRequiredDefaultDescription
node_keypathstringyesnoneNode to start from.
max_depthqueryintegerno50Clamped to the range 1 to 200 by the server.

Response 200:

FieldTypeDescription
originstringThe requested key.
origin_quarantinedbooleanContainment flag for the origin.
affected_nodes[]arrayid, label, type, provider, host, schema, depth (hops from origin), quarantined. A key with no stored node gets type of unknown.
edges[]arrayThe edges walked: id, source, target, label.
truncatedbooleanTrue when max_depth stopped the walk with more nodes still reachable.

An unknown node_key is not an error. You get 200 with an empty affected_nodes. Status codes: 200.

bash
curl "https://mmune.example.com/api/v1/lineage/impact/pg_ab12cd34.sales.orders?max_depth=10" \
  -H "Authorization: Bearer <TOKEN>"

Lineage sync router ​

The sync routes are /api/v1/lineage/sync and /api/v1/lineage/sync/now.

Both endpoints take the same body.

NameTypeRequiredDefaultDescription
include_unconfiguredbooleannotrueAlso try discovered integrations that have no saved configuration. Credentials come from the integration's credential profile or from default_credentials, and an integration with no usable credentials is skipped.
default_credentialsobject or nullnonullMap of provider name to credential object, for example {"postgresql": {"username": "<USER>", "password": "<PASSWORD>"}}. Treat it as a secret.

POST /api/v1/lineage/sync ​

Starts the sync in the background and returns at once. Needs: write. Response 200: {"message": "Lineage sync started", "job_id": "<uuid>", "status": "RUNNING"}. On completion the job result holds the same object that sync/now returns. Poll it with GET /api/v1/discovery/status/{job_id} (see Integrations and discovery). The results field then holds the sync result rather than schemas. See Async jobs.

bash
curl -X POST https://mmune.example.com/api/v1/lineage/sync \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"include_unconfigured": false}'

POST /api/v1/lineage/sync/now ​

Runs the sync inside the request and waits. Needs: write. Response 200:

FieldTypeDescription
nodes_createdintegerNodes written.
edges_createdintegerEdges written.
integrations_processed, integrations_succeeded, integrations_failedintegerCounts over discovered database integrations.
errorsarray of stringOne message per failed integration.

Status codes: 200, 500 (the whole sync failed).

bash
curl -X POST https://mmune.example.com/api/v1/lineage/sync/now \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" -d '{}'

Semantic router ​

Path prefix: /api/v1/semantic. See Semantic mapping model.

POST /api/v1/semantic/extract-intent ​

Extracts the business intent of a field and registers it in the intent registry. Needs: write.

NameTypeRequiredDefaultDescription
field_namestringyesnoneField to analyze.
field_descriptionstringnonullContext.
sample_valuesarraynonullExample values.
data_typestringnonullColumn type.
use_llmbooleannotrueAllow model enrichment on top of the pattern floor.

Response 200: {"intent": {"category", "concept", "semantic_signature", "reasoning"}, "confidence": 0.0 to 1.0}. Status codes: 200, 422, 500.

bash
curl -X POST https://mmune.example.com/api/v1/semantic/extract-intent \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"field_name": "cust_email_addr", "data_type": "varchar", "use_llm": false}'

GET /api/v1/semantic/mappings-by-intent/ ​

Lists persisted mappings whose business_intent equals the given concept exactly, highest confidence_score first. Needs: read.

NameInTypeRequiredDefaultDescription
intentpathstringyesnoneConcept name, URL-encoded (for example Customer%20Name).
limitqueryintegerno100Maximum rows.

Response 200: {"intent": "<intent>", "mappings": [<semantic mapping>, ...], "count": <n>}. Status codes: 200, 500.

bash
curl "https://mmune.example.com/api/v1/semantic/mappings-by-intent/Customer%20Name?limit=20" \
  -H "Authorization: Bearer <TOKEN>"

POST /api/v1/semantic/find-semantic-match ​

Looks for an existing mapping from source_field to one of the target_fields, first directly, then through intent signatures. Needs: read.

NameTypeRequiredDefaultDescription
source_fieldstringyesnoneSource field name.
target_fieldsarray of stringyesnoneCandidate target names.
source_vendorstringnonullNarrow to one source system.
target_modelstringnonullNarrow to one target model.
min_confidencenumberno0.5Minimum confidence for the direct lookup.

Response 200: match_found (boolean), mapping (semantic mapping object or null), confidence (number or null), reasoning (string or null). A miss is 200 with match_found false. Status codes: 200, 422, 500.

bash
curl -X POST https://mmune.example.com/api/v1/semantic/find-semantic-match \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"source_field": "cust_email", "target_fields": ["email", "contact_email"], "min_confidence": 0.6}'

GET /api/v1/semantic/mapping-history/ ​

Needs: read. Path parameter mapping_id (string). Response 200: {"mapping_id": "<id>", "versions": [<version>, ...]} with versions newest first. An unknown id returns an empty versions array with status 200. Status codes: 200, 500.

bash
curl https://mmune.example.com/api/v1/semantic/mapping-history/5f0c2a7e-0000-0000-0000-000000000000 \
  -H "Authorization: Bearer <TOKEN>"

GET /api/v1/semantic/intent-registry/stats ​

Counts from the intent registry, which is filled by extract-intent calls and is separate from the stored semantic mappings. It is not retained across a restart. Needs: read. Response 200: total_signatures (integer), by_category (object of category to count), total_concepts (integer). Status codes: 200, 500.

bash
curl https://mmune.example.com/api/v1/semantic/intent-registry/stats -H "Authorization: Bearer <TOKEN>"

GET /api/v1/semantic/intent-registry/signatures ​

Lists signatures held in the intent registry. Needs: read.

NameInTypeRequiredDefaultDescription
categoryquerystringnononeOne of the category values in the semantic mapping table.
limitqueryintegerno100Applied to the returned list only.

Response 200: {"signatures": [{"signature", "category", "concept", "examples"}], "count": <total before limit>}. Status codes: 200, 500.

bash
curl "https://mmune.example.com/api/v1/semantic/intent-registry/signatures?category=customer&limit=25" \
  -H "Authorization: Bearer <TOKEN>"

GET /api/v1/semantic/stats ​

Explorer summary computed from persisted semantic mappings. Mappings are grouped by (business_intent_category or "other", business_intent or concept or "unknown_intent"), and each group is one "intent". Needs: read. Response 200: total_intents (integer), categories (integer, distinct categories), avg_confidence (number, mean of the groups' average confidence_score, 0.0 when empty). Status codes: 200, 500.

bash
curl https://mmune.example.com/api/v1/semantic/stats -H "Authorization: Bearer <TOKEN>"

GET /api/v1/semantic/signatures ​

One row per intent group (see above), sorted by category then concept. Needs: read. Response 200: {"signatures": [...], "count": <n>} where each row has signature_hash (16 hex characters, a stable hash of the group's signature), category, concept, field_name_pattern (up to 12 sorted source field names joined by |), description_keywords (up to 8 keywords). Status codes: 200, 500.

bash
curl https://mmune.example.com/api/v1/semantic/signatures -H "Authorization: Bearer <TOKEN>"

GET /api/v1/semantic/signatures/{signature_hash}/matches ​

Lists the source systems that hold fields for one signature, with the matched fields. Needs: read. Path parameter signature_hash (string from GET /signatures). Response 200:

FieldTypeDescription
signature_hashstringEcho.
matches[]arrayOne item per source system, sorted by number of matched fields descending, then name.
matches[].system_idstringThe mapping's source_vendor, or unknown.
matches[].system_name, .providerstringDisplay name and provider of the system, falling back to system_id.
matches[].matched_fieldsarray of stringtable.field (or field when there is no table), de-duplicated and sorted.
countintegerNumber of systems.

An unknown hash returns an empty matches array with status 200. Status codes: 200, 500.

bash
curl https://mmune.example.com/api/v1/semantic/signatures/3a7bd3e2360a3d29/matches \
  -H "Authorization: Bearer <TOKEN>"

Duplicates router ​

Path prefix: /api/v1/duplicates. Groups, resolutions and the handling mode are not retained across a restart. Besides manual detection, groups are also filled by an opt-in auto-scan when MMUNE_AUTO_DUPLICATE_SCAN is enabled.

Shared shapes. A duplicate group has group_id (string), record_ids (array of string), primary_record_id (string or null), similarity_scores (object keyed "<recordA>:<recordB>", value 0.0 to 1.0), match_types (object with the same keys, value exact at 0.95 and above, semantic at 0.8 and above, fuzzy at 0.6 and above, otherwise partial), business_intent (string or null), detected_at (ISO 8601), resolved (boolean) and resolved_at (ISO 8601 or null). A record's id is read from the first present key among id, record_id, _id, ID, Id. Without one, a hash of the record is used. Valid business_intent values are the category list in the semantic mapping table, case-insensitive.

GET /api/v1/duplicates/config ​

Needs: read. Response 200: {"mode": "flag"} or {"mode": "block"}. The default comes from the DUPLICATE_MODE environment variable and falls back to flag. In flag mode duplicates are surfaced and writes are allowed. In block mode validate-before-write rejects them.

bash
curl https://mmune.example.com/api/v1/duplicates/config -H "Authorization: Bearer <TOKEN>"

PUT /api/v1/duplicates/config ​

Changes the duplicate handling mode. Needs: write.

NameTypeRequiredDefaultDescription
modestringnonullflag or block. Null leaves the mode unchanged.

Response 200: {"mode": "<current mode>"}. Status codes: 200, 400 (mode must be 'flag' or 'block'), 422.

bash
curl -X PUT https://mmune.example.com/api/v1/duplicates/config \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" -d '{"mode": "block"}'

POST /api/v1/duplicates/validate-before-write ​

Pipeline gate to call before writing a record to a target. Needs: write. The body is embedded, so each field sits at the top level of the JSON object.

NameTypeRequiredDescription
recordobjectyesThe record about to be written.
existing_recordsarray of objectnoRecords to compare against.
business_intentstringnoCategory that selects the key fields.

Response 200, one of three shapes. With no duplicate: {"allowed": true, "duplicate_found": false}. In block mode with a duplicate: {"allowed": false, "duplicate_found": true, "duplicate_group": <group>, "blocked_reason": "duplicate_detected"}. In flag mode with a duplicate: {"allowed": true, "duplicate_found": true, "duplicate_group": <group>, "should_flag": true}. Status codes: 200, 400 (invalid business_intent), 422, 500.

bash
curl -X POST https://mmune.example.com/api/v1/duplicates/validate-before-write \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"record": {"id": "n-1", "email": "dana@example.com", "name": "Dana Levi"}, "existing_records": [{"id": "e-7", "email": "dana@example.com", "name": "Dana Levi"}], "business_intent": "customer"}'

GET /api/v1/duplicates/groups ​

Lists known groups, newest first. This is the read path the Duplicates tab uses. There is no statistics endpoint, so count groups on the client if you need totals. Needs: read.

NameInTypeRequiredDefaultDescription
resolvedquerybooleannononeTrue for resolved groups only, false for open ones, omitted for all.

Response 200: array of duplicate groups.

bash
curl "https://mmune.example.com/api/v1/duplicates/groups?resolved=false" -H "Authorization: Bearer <TOKEN>"

POST /api/v1/duplicates/detect ​

Detects duplicate groups in the records you send and stores them. Needs: write. Fewer than two records returns an empty array.

NameTypeRequiredDefaultDescription
recordsarray of objectyesnoneRecords to compare, pairwise.
business_intentstringnonullCategory that selects the key fields.

Response 200: array of duplicate groups. Status codes: 200, 400 (invalid business_intent), 422, 500.

bash
curl -X POST https://mmune.example.com/api/v1/duplicates/detect \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"records": [{"id": "a1", "email": "dana@example.com", "name": "Dana Levi"}, {"id": "b2", "email": "dana@example.com", "name": "Dana  Levi"}], "business_intent": "customer"}'

GET /api/v1/duplicates/ ​

Returns the group that contains a record. Needs: read. Path parameter record_id (string). Response 200: a duplicate group, or the JSON value null when the record is in no group. Status codes: 200, 500.

bash
curl https://mmune.example.com/api/v1/duplicates/a1 -H "Authorization: Bearer <TOKEN>"

POST /api/v1/duplicates/resolve ​

Records a decision for a group and marks the group resolved with the chosen primary. It only records the decision in mmune and does not merge or delete rows in any source system. Needs: write.

NameTypeRequiredDefaultDescription
group_idstringyesnoneGroup to resolve.
primary_record_idstringyesnoneRecord to keep.
actionstringyesnonemerged, kept_separate, deleted, ignored or pending (case-insensitive).
merged_record_idsarray of stringno[]Records folded into the primary.
deleted_record_idsarray of stringno[]Records marked deleted.
resolved_bystringnonullFree text name of the person.
notesstringnonullFree text.

Response 200: resolution_id, group_id, status, primary_record_id, merged_record_ids, deleted_record_ids, resolved_by, resolved_at, notes. Status codes: 200, 400 (invalid action, or the group id is not found), 422, 500.

bash
curl -X POST https://mmune.example.com/api/v1/duplicates/resolve \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"group_id": "<GROUP_ID>", "primary_record_id": "a1", "action": "merged", "merged_record_ids": ["b2"], "resolved_by": "dana"}'

GET /api/v1/duplicates/resolutions/history ​

Needs: read.

NameInTypeRequiredDefaultDescription
group_idquerystringnononeRestrict to one group.
limitqueryintegerno100Maximum rows, newest first.

Response 200: {"resolutions": [<resolution>, ...], "count": <n>} where each resolution has the fields listed under resolve.

bash
curl "https://mmune.example.com/api/v1/duplicates/resolutions/history?limit=20" -H "Authorization: Bearer <TOKEN>"

POST /api/v1/duplicates/check ​

Real-time check of one record. Needs: write. The body is embedded with the same fields as validate-before-write: record (object, required), existing_records (array, optional), business_intent (string, optional). Response 200: {"duplicate_found": false, "message": "No duplicates found"} or {"duplicate_found": true, "group": <group>}. Unlike validate-before-write, it does not consult the duplicate mode. Status codes: 200, 400, 422, 500.

bash
curl -X POST https://mmune.example.com/api/v1/duplicates/check \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"record": {"id": "n-2", "email": "dana@example.com"}, "existing_records": [{"id": "e-7", "email": "dana@example.com"}]}'

Recommendations router ​

Path prefix: /api/v1/recommendations. These are the estate advisor findings.

A recommendation (RecommendationOut) has id (integer), integration_id, provider, category, recommendation_type (falls back to category), entity, entity_type, severity (critical, high, medium or low), title, description, suggested_action, confidence (number), source (rule or llm), status (active, dismissed or resolved), detected_at (ISO 8601), evidence (object) and impact (object or null).

GET /api/v1/recommendations ​

Lists recommendations, newest first. Needs: read. The path has no trailing slash.

NameInTypeRequiredDefaultDescription
integration_idquerystringnononeFilter.
providerquerystringnononeFilter.
recommendation_typequerystringnononeMatches either recommendation_type or category.
categoryquerystringnononeLegacy alias, used only when recommendation_type is absent.
severityquerystringnononeFilter.
statusquerystringnoactiveWhen omitted, only active rows are returned.
limitqueryintegerno200Maximum 500.

Response 200: a bare array of recommendations.

bash
curl "https://mmune.example.com/api/v1/recommendations?severity=high&status=active" -H "Authorization: Bearer <TOKEN>"

POST /api/v1/recommendations/scan ​

Runs the advisor against matching integrations and waits for it. Needs: write.

NameTypeRequiredDefaultDescription
integration_idstringnonullScan one integration. Takes precedence over provider.
providerstringnonullScan every integration of one provider.
analyzer_typesarray of stringnonullRestrict analyzers by recommendation_type or analyzer id, for example dead_flow, performance, sap_topology.

With neither integration_id nor provider, the scan covers integrations in the registered or configured state. Response 200: {"message": "Scan complete: N new finding(s)", "scanned": <integrations>, "new_findings": <n>, "findings": [...]}. When nothing matches: {"message": "No matching integrations found", "scanned": 0, "findings": []} (no new_findings key). An integration that cannot be scanned is skipped. findings lists the new recommendations the scan created. Status codes: 200, 500.

bash
curl -X POST https://mmune.example.com/api/v1/recommendations/scan \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"provider": "postgresql", "analyzer_types": ["dead_flow"]}'

PATCH /api/v1/recommendations/ ​

Needs: write.

NameInTypeRequiredDescription
recommendation_idpathintegeryesRecommendation id.
statusbodystringyesactive, dismissed or resolved.

Response 200: {"id": <id>, "status": "<status>", "message": "Updated"}. Status codes: 200, 400 (status not one of the three), 404 (Recommendation not found), 422, 500.

bash
curl -X PATCH https://mmune.example.com/api/v1/recommendations/42 \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" -d '{"status": "dismissed"}'

Reconciliation router ​

Path prefix: /api/v1/reconciliation. See Reconciliation model first. Error bodies from this router put a structured object in detail with a code and a message, for example {"detail": {"code": "illegal_transition", "message": "..."}}.

Pairing object (returned by every pairing endpoint): id, name, status, every spec field listed under POST /pairings, plus proposed_by, activated_by, proposal_evidence, index_check, activated_at, last_checked_at, last_check_status (ok or error), last_check_error, last_check_stats (a_ms, b_ms, a_rows_aggregated, b_rows_aggregated, partitions, settled), created_at, updated_at and open_findings_count. Timestamps are ISO 8601.

GET /api/v1/reconciliation/pairings ​

Needs: read.

NameInTypeRequiredDefaultDescription
statusquerystringnononeproposed, active, paused or rejected. An unknown value returns an empty list.

Response 200: {"pairings": [<pairing>, ...], "count": <n>}, newest first.

bash
curl "https://mmune.example.com/api/v1/reconciliation/pairings?status=active" -H "Authorization: Bearer <TOKEN>"

GET /api/v1/reconciliation/pairings/ ​

Needs: read. Response 200: the pairing object plus partitions, an array with one item per tracked partition.

Partition fieldTypeDescription
partition_refintegerState id. It equals partition_ref on findings.
partition_valuestringThe literal partition value.
a_row_count, b_row_count, b_key_countinteger or nullRow counts per side. b_key_count counts distinct keys on B.
a_sum_minor, b_sum_minorinteger or nullMeasure sums in minor units.
a_last_activity_atstring or nullLatest A-side timestamp seen.
a_age_secondsinteger or nullAge of that timestamp at the last check.
settledbooleanTrue once the A-side age reaches the settle window.
agreesboolean or nullResult of the last comparison.
consecutive_disagreementsintegerConsecutive disagreeing checks.
first_disagreed_at, last_checked_atstring or nullTimestamps.
open_finding_event_idinteger or nullOpen finding, if any.

Status codes: 200, 404 (pairing not found: <id>), 500.

bash
curl https://mmune.example.com/api/v1/reconciliation/pairings/<PAIRING_ID> -H "Authorization: Bearer <TOKEN>"

POST /api/v1/reconciliation/pairings ​

Declares a pairing in proposed status. Needs: write. Every table and column you name must already exist in the lineage catalog. The server looks up the table node {integration_id}.{database}.{table} and checks each declared column against that node's recorded columns. Response 201: the pairing object. A person's activate call is the only way a pairing becomes active. The range and value rules in the table below (cadence_seconds, lookback_partitions, lookback_days, the tolerance_* and settle_window_seconds minimums, and the allowed values of partition_value_type, a_timestamp_kind and key_normalizer) are checked at activation, and an invalid value returns 422 activation_incomplete.

NameTypeRequiredDefaultDescription
namestringyesnone1 to 200 characters.
a_integration_idstringyesnoneIntegration id of side A (the source of truth, for example billing).
b_integration_idstringyesnoneIntegration id of side B (the receiving system, for example the ledger). Must differ from A at activation. Findings are attributed to this side.
a_table, b_tablestringyesnoneTable names.
a_key_column, b_key_columnstringyesnoneJoin key columns. A is expected to be unique per key. B may repeat a key and is summed.
a_measure_column, b_measure_columnstringyesnoneAmount columns.
a_database, b_databasestringyesnoneDatabase names. Omitting one returns 422 missing_database.
a_schema, b_schemastringnonullSchema names.
a_measure_scale, b_measure_scaleintegernonullRequired at activation, 0 to 6.
minor_unit_exponentintegerno2Decimal places used to format *_display strings.
currencystringnonullOne currency per pairing. No conversion is done.
a_partition_column, b_partition_columnstringnonullRequired at activation. Must not equal the key column.
partition_value_typestringnostringstring or integer.
a_timestamp_columnstringnonullRequired at activation. Latest activity on side A.
a_timestamp_kindstringnoutcutc or source_local.
settle_window_secondsintegernonullRequired at activation, 0 or more. How old the newest A-side activity must be before a partition counts as settled.
cadence_secondsintegerno90060 or more.
tolerance_per_partition_minorintegerno0Absolute tolerance on a partition's sum, in minor units. 0 or more.
tolerance_per_key_minorintegerno0Absolute tolerance per key, in minor units. 0 or more.
lookback_partitionsintegerno101 to 50.
lookback_daysintegerno351 to 366.
key_normalizerstringnonullstrip_leading_zeros, strip_nonalnum, lowercase or digits_only. Applied to join keys before comparing.

Status codes: 201, 422 (name length or field type validation, or lineage validation with detail.code of missing_database, missing_lineage_table or missing_lineage_column), 500.

bash
curl -X POST https://mmune.example.com/api/v1/reconciliation/pairings \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{
    "name": "Billing to GL invoices",
    "a_integration_id": "mssql_aa11bb22", "a_database": "billing", "a_table": "invoices",
    "a_key_column": "invoice_no", "a_measure_column": "amount",
    "b_integration_id": "pg_cc33dd44", "b_database": "finance", "b_table": "gl_entries",
    "b_key_column": "invoice_no", "b_measure_column": "amount",
    "a_measure_scale": 2, "b_measure_scale": 2, "currency": "USD",
    "a_partition_column": "bill_date", "b_partition_column": "bill_date",
    "a_timestamp_column": "updated_at", "settle_window_seconds": 3600
  }'

PATCH /api/v1/reconciliation/pairings/ ​

Edits a pairing. Needs: write. The body accepts any subset of the field names above (all optional, same types and rules). Unknown keys are rejected with 422. Only proposed and paused pairings are editable. The merged pairing is re-validated against the lineage catalog. An empty body returns the pairing unchanged. Response 200: the pairing object. Status codes: 200, 404, 409 (pairing_not_editable, with the current status), 422, 500.

bash
curl -X PATCH https://mmune.example.com/api/v1/reconciliation/pairings/<PAIRING_ID> \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"settle_window_seconds": 7200, "cadence_seconds": 600}'

POST /api/v1/reconciliation/pairings/{pairing_id}/activate ​

Validates the pairing, checks that each side has an index that supports the partition predicate, and activates it. Needs: write. The body is empty or {}. There is no override for a failed index check. On success the pairing becomes active, activated_by is set to the caller, last_checked_at is cleared and the runner picks it up on its next tick. Response 200: the pairing object.

Status codes: 200, 404, 409, 422. The 409 detail.code values are illegal_transition, index_check_failed (the response carries index_check and a missing list per side, and the result is saved on the pairing) and missing_source_credentials. The 422 detail.code is activation_incomplete with a problems array of readable messages, for example a_partition_column is required before activation.

bash
curl -X POST https://mmune.example.com/api/v1/reconciliation/pairings/<PAIRING_ID>/activate \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" -d '{}'

POST /api/v1/reconciliation/pairings/{pairing_id}/pause ​

Moves an active pairing to paused. Needs: write. Empty body. Response 200: the pairing object. Status codes: 200, 404, 409 (illegal_transition).

bash
curl -X POST https://mmune.example.com/api/v1/reconciliation/pairings/<PAIRING_ID>/pause \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" -d '{}'

POST /api/v1/reconciliation/pairings/{pairing_id}/reject ​

Moves a proposed or paused pairing to rejected, which is final. Needs: write. Empty body. Response 200: the pairing object. Status codes: 200, 404, 409 (illegal_transition).

bash
curl -X POST https://mmune.example.com/api/v1/reconciliation/pairings/<PAIRING_ID>/reject \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" -d '{}'

GET /api/v1/reconciliation/findings ​

Lists findings, newest first. Needs: read.

NameInTypeRequiredDefaultDescription
pairing_idquerystringnononeRestrict to one pairing.
include_resolvedquerybooleannofalseInclude findings that later checks cleared.
limitqueryintegerno100Range 1 to 500.

Response 200: {"findings": [...], "count": <n>}. A finding is a detection event of the reconciliation dimension. Open findings are de-duplicated per partition and resolve on their own when a later check agrees, which is how late postings clear.

FieldTypeDescription
idintegerDetection event id.
integration_idstringThe B-side integration.
detected_at, resolved_atstring, string or nullTimestamps.
impact_estimateany or nullImpact estimate when one was computed.
pairing_id, pairing_namestringThe pairing.
a_integration_id, b_integration_idstringBoth sides.
a_table, b_tablestringQualified database.schema.table, omitting empty parts.
partition_column_a, partition_column_bstringPartition columns.
partition_refintegerMatches partition_ref in the pairing's partitions.
partitionstringThe literal partition value.
lookup_sqlobject{"a": "<read-only SQL>", "b": "<read-only SQL>"} for the operator to run in their own tooling.
comparisonstringaggregate, key_level or fingerprint.
a_count, b_count, b_row_count, count_deltaintegerA key count, B distinct key count, B row count and the count difference.
a_sum_minor, b_sum_minor, sum_delta_minorintegerSums and difference in minor units.
minor_unit_exponent, currencyinteger, string or nullFormatting hints.
sum_delta_display, measured_amount_minor, measured_amount_displaystring, integer, stringFormatted difference and its absolute value.
settle_window_seconds, a_age_secondsintegerSettle window and the A-side age at detection.
detailstringHuman-readable summary.
row_countintegerAbsolute count difference. Present only when count_delta is not zero.

Fields appear only when they apply. Status codes: 200, 500.

bash
curl "https://mmune.example.com/api/v1/reconciliation/findings?pairing_id=<PAIRING_ID>&limit=20" \
  -H "Authorization: Bearer <TOKEN>"

Analysis router ​

Path prefix: /api/v1/analysis. A question first goes to a deterministic fast path (a built-in lookup that calls a read-only data tool, no model involved). If that does not answer, a configured, healthy model is used. When no AI provider is available the endpoint answers with navigation links into the console instead of generated text. Actions that change state are performed only for callers with the write permission or the admin role, even though the endpoint itself needs only read.

Request body for both endpoints:

NameTypeRequiredDescription
project_idstringyesProject identifier.
questionstringyesThe question.
historyarray of objectnoEarlier turns as {"role": "user", "content": "..."}. role defaults to user.

POST /api/v1/analysis/chat ​

Needs: read. Response 200:

FieldTypeDescription
answerstringAlways populated.
blocksarray of objectStructured payloads such as tables. Empty for plain answers.
console_linksarrayItems with label, route, tab (nullable) and focus_node_id (nullable, set when a tool resolved a specific lineage node).
pathstringfast (deterministic, no model) or slow (model path).
used_toolsarray of stringTools that ran.

Status codes: 200, 422.

bash
curl -X POST https://mmune.example.com/api/v1/analysis/chat \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"project_id": "default", "question": "Which integrations are unhealthy right now?"}'

POST /api/v1/analysis/chat/stream ​

Same inputs and permission as chat. The response is text/event-stream, and each frame is a line data: <json> followed by a blank line. Frame types: meta (used_tools, path), delta (text, one or more), block (block, zero or more, fast path only), done (path, console_links) and error (message). On the slow path the answer text arrives as a single delta after the model has finished.

bash
curl -N -X POST https://mmune.example.com/api/v1/analysis/chat/stream \
  -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"project_id": "default", "question": "Show lineage impact for pg_ab12cd34.sales.orders"}'

Worked examples ​

Each example assumes the token is in the MMUNE_TOKEN environment variable.

Example 1: Read the lineage graph and filter by node ​

The graph endpoint has no filter parameters, so the filter runs on your side. The first snippet keeps one integration's subgraph. The second asks the server for the downstream blast radius of one table.

Python:

python
import os
import requests

BASE = "https://mmune.example.com"
HEADERS = {"Authorization": f"Bearer {os.environ['MMUNE_TOKEN']}"}

graph = requests.get(f"{BASE}/api/v1/lineage/graph", headers=HEADERS, timeout=30)
graph.raise_for_status()
data = graph.json()

integration_id = "pg_ab12cd34"  # the source node's key
prefix = f"{integration_id}."
# Nodes written by lineage sync carry no id prefix, so also match on metadata.
nodes = [
    n for n in data["nodes"]
    if n["id"] == integration_id
    or n["id"].startswith(prefix)
    or (n["metadata"] or {}).get("integration_id") == integration_id
]
keys = {n["id"] for n in nodes}
edges = [e for e in data["edges"] if e["source"] in keys or e["target"] in keys]

for n in nodes:
    if n["type"] == "table":
        meta = n["metadata"] or {}
        # Read provenance with a default of "unknown", never "exact".
        print(n["id"], meta.get("row_count"), meta.get("row_count_source", "unknown"))

impact = requests.get(
    f"{BASE}/api/v1/lineage/impact/{integration_id}.sales.orders",
    params={"max_depth": 10},
    headers=HEADERS,
    timeout=30,
)
impact.raise_for_status()
result = impact.json()
print(len(result["affected_nodes"]), "affected, truncated:", result["truncated"])

TypeScript:

typescript
const BASE = "https://mmune.example.com";
const TOKEN = process.env.MMUNE_TOKEN!;

async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...(init.headers ?? {}) },
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return (await res.json()) as T;
}

interface GraphNode { id: string; type: string; metadata: Record<string, unknown> | null }
interface GraphEdge { id: string; source: string; target: string; label: string }

const integrationId = "pg_ab12cd34";
const graph = await api<{ nodes: GraphNode[]; edges: GraphEdge[] }>("/api/v1/lineage/graph");
const nodes = graph.nodes.filter((n) => n.id === integrationId || n.id.startsWith(`${integrationId}.`));
const keys = new Set(nodes.map((n) => n.id));
const edges = graph.edges.filter((e) => keys.has(e.source) || keys.has(e.target));

for (const n of nodes.filter((n) => n.type === "table")) {
  const source = (n.metadata?.row_count_source as string | undefined) ?? "unknown";
  console.log(n.id, n.metadata?.row_count, source);
}

const impact = await api<{ affected_nodes: unknown[]; truncated: boolean }>(
  `/api/v1/lineage/impact/${integrationId}.sales.orders?max_depth=10`,
);
console.log(impact.affected_nodes.length, "affected, truncated:", impact.truncated, edges.length);

Example 2: Save a mapping and list mappings ​

There is no endpoint that creates a semantic mapping directly, because mmune creates them during introspection. What you can store through this API is a mapping result (POST /mapping/map) and operator feedback on it (POST /mapping/feedback). You can then list stored mappings and read the semantic mappings that share an intent.

Python:

python
import os
from urllib.parse import quote

import requests

BASE = "https://mmune.example.com"
HEADERS = {"Authorization": f"Bearer {os.environ['MMUNE_TOKEN']}"}

record = {
    "vendorId": "crm-eu",
    "vendorName": "CRM Europe",
    "objectType": "customer",
    "rawData": {"first_name": "Dana", "last_name": "Levi", "email_address": "dana@example.com"},
    "metadata": {"recordId": "c-1001", "lastModified": "2026-10-01T09:00:00Z", "retrievedAt": "2026-10-04T08:00:00Z"},
}
saved = requests.post(f"{BASE}/api/v1/mapping/map", json=record, headers=HEADERS, timeout=60)
saved.raise_for_status()
result = saved.json()

# Record the operator's judgment on the first mapped field.
first = result["fieldMappings"][0]
feedback = requests.post(
    f"{BASE}/api/v1/mapping/feedback",
    json={
        "mapping_id": result["mappingId"],
        "source_field": first["sourceField"],
        "target_field": first["targetField"],
        "feedback_type": "correct",
        "ai_confidence": first["confidence"],
    },
    headers=HEADERS,
    timeout=30,
)
feedback.raise_for_status()  # 201

files = requests.get(f"{BASE}/api/v1/mapping/saved", headers=HEADERS, timeout=30).json()
print(files)  # newest first

by_intent = requests.get(
    f"{BASE}/api/v1/semantic/mappings-by-intent/{quote('Customer Name')}",
    params={"limit": 20},
    headers=HEADERS,
    timeout=30,
)
by_intent.raise_for_status()
for m in by_intent.json()["mappings"]:
    print(m["source_vendor"], m["source_field"], "->", m["target_field"], m["confidence_score"])

TypeScript:

typescript
// Uses the api() helper from Example 1.
const record = {
  vendorId: "crm-eu",
  vendorName: "CRM Europe",
  objectType: "customer",
  rawData: { first_name: "Dana", last_name: "Levi", email_address: "dana@example.com" },
  metadata: { recordId: "c-1001", lastModified: "2026-10-01T09:00:00Z", retrievedAt: "2026-10-04T08:00:00Z" },
};

const result = await api<{ mappingId: string; fieldMappings: { sourceField: string; targetField: string; confidence: number }[] }>(
  "/api/v1/mapping/map",
  { method: "POST", body: JSON.stringify(record) },
);

const first = result.fieldMappings[0];
await api("/api/v1/mapping/feedback", {
  method: "POST",
  body: JSON.stringify({
    mapping_id: result.mappingId,
    source_field: first.sourceField,
    target_field: first.targetField,
    feedback_type: "correct",
    ai_confidence: first.confidence,
  }),
});

const files = await api<string[]>("/api/v1/mapping/saved");
console.log(files);

const byIntent = await api<{ mappings: { source_vendor: string; source_field: string; target_field: string; confidence_score: number }[] }>(
  `/api/v1/semantic/mappings-by-intent/${encodeURIComponent("Customer Name")}?limit=20`,
);
for (const m of byIntent.mappings) console.log(m.source_vendor, m.source_field, "->", m.target_field, m.confidence_score);

Example 3: Run a cross-system reconciliation and read the result ​

A reconciliation has no "run now" call. You declare a pairing, activate it, and the background runner checks it on its cadence. Then you poll the pairing until last_checked_at is set and read the findings. The tables and columns must already be in the lineage graph.

Python:

python
import os
import time

import requests

BASE = "https://mmune.example.com"
HEADERS = {"Authorization": f"Bearer {os.environ['MMUNE_TOKEN']}"}


def call(method: str, path: str, **kwargs):
    res = requests.request(method, f"{BASE}{path}", headers=HEADERS, timeout=60, **kwargs)
    if not res.ok:
        raise RuntimeError(f"{res.status_code} {res.text}")
    return res.json()


spec = {
    "name": "Billing to GL invoices",
    "a_integration_id": "mssql_aa11bb22", "a_database": "billing", "a_table": "invoices",
    "a_key_column": "invoice_no", "a_measure_column": "amount", "a_measure_scale": 2,
    "a_partition_column": "bill_date", "a_timestamp_column": "updated_at",
    "b_integration_id": "pg_cc33dd44", "b_database": "finance", "b_table": "gl_entries",
    "b_key_column": "invoice_no", "b_measure_column": "amount", "b_measure_scale": 2,
    "b_partition_column": "bill_date",
    "currency": "USD", "settle_window_seconds": 3600,
}
pairing = call("POST", "/api/v1/reconciliation/pairings", json=spec)
pid = pairing["id"]

# A 409 here means the supporting indexes are missing (index_check_failed). There is no override.
call("POST", f"/api/v1/reconciliation/pairings/{pid}/activate", json={})

while True:
    state = call("GET", f"/api/v1/reconciliation/pairings/{pid}")
    if state["last_checked_at"]:
        break
    time.sleep(30)

if state["last_check_status"] == "error":
    raise RuntimeError(state["last_check_error"])

findings = call("GET", "/api/v1/reconciliation/findings", params={"pairing_id": pid})
for f in findings["findings"]:
    print(f["partition"], f["comparison"], f["sum_delta_display"], f["detail"])
    print("  A:", f["lookup_sql"]["a"])
    print("  B:", f["lookup_sql"]["b"])
if not findings["findings"]:
    print("No open disagreements across", len(state["partitions"]), "partitions")

TypeScript:

typescript
// Uses the api() helper from Example 1.
const spec = {
  name: "Billing to GL invoices",
  a_integration_id: "mssql_aa11bb22", a_database: "billing", a_table: "invoices",
  a_key_column: "invoice_no", a_measure_column: "amount", a_measure_scale: 2,
  a_partition_column: "bill_date", a_timestamp_column: "updated_at",
  b_integration_id: "pg_cc33dd44", b_database: "finance", b_table: "gl_entries",
  b_key_column: "invoice_no", b_measure_column: "amount", b_measure_scale: 2,
  b_partition_column: "bill_date",
  currency: "USD", settle_window_seconds: 3600,
};

const pairing = await api<{ id: string }>("/api/v1/reconciliation/pairings", {
  method: "POST",
  body: JSON.stringify(spec),
});
await api(`/api/v1/reconciliation/pairings/${pairing.id}/activate`, { method: "POST", body: "{}" });

type PairingState = { last_checked_at: string | null; last_check_status: string | null; last_check_error: string | null; partitions: unknown[] };
let state: PairingState;
do {
  await new Promise((r) => setTimeout(r, 30_000));
  state = await api<PairingState>(`/api/v1/reconciliation/pairings/${pairing.id}`);
} while (!state.last_checked_at);

if (state.last_check_status === "error") throw new Error(state.last_check_error ?? "check failed");

const { findings } = await api<{ findings: { partition: string; comparison: string; sum_delta_display: string; detail: string }[] }>(
  `/api/v1/reconciliation/findings?pairing_id=${pairing.id}`,
);
for (const f of findings) console.log(f.partition, f.comparison, f.sum_delta_display, f.detail);

Example 4: Fetch duplicates and recommendations ​

Python:

python
import os

import requests

BASE = "https://mmune.example.com"
HEADERS = {"Authorization": f"Bearer {os.environ['MMUNE_TOKEN']}"}

# Detect duplicates in a batch, then list open groups.
records = [
    {"id": "a1", "email": "dana@example.com", "name": "Dana Levi"},
    {"id": "b2", "email": "dana@example.com", "name": "Dana  Levi"},
    {"id": "c3", "email": "omer@example.com", "name": "Omer Katz"},
]
detected = requests.post(
    f"{BASE}/api/v1/duplicates/detect",
    json={"records": records, "business_intent": "customer"},
    headers=HEADERS,
    timeout=60,
)
detected.raise_for_status()

groups = requests.get(
    f"{BASE}/api/v1/duplicates/groups", params={"resolved": "false"}, headers=HEADERS, timeout=30
).json()
for g in groups:
    print(g["group_id"], g["record_ids"], g["match_types"])

# Run the advisor for one provider, then read high-severity active recommendations.
scan = requests.post(
    f"{BASE}/api/v1/recommendations/scan", json={"provider": "postgresql"}, headers=HEADERS, timeout=300
)
scan.raise_for_status()
print(scan.json()["message"])

recs = requests.get(
    f"{BASE}/api/v1/recommendations",
    params={"severity": "high", "status": "active", "limit": 50},
    headers=HEADERS,
    timeout=30,
)
recs.raise_for_status()
for r in recs.json():
    print(r["id"], r["severity"], r["integration_id"], r["title"])

TypeScript:

typescript
// Uses the api() helper from Example 1.
const records = [
  { id: "a1", email: "dana@example.com", name: "Dana Levi" },
  { id: "b2", email: "dana@example.com", name: "Dana  Levi" },
  { id: "c3", email: "omer@example.com", name: "Omer Katz" },
];
await api("/api/v1/duplicates/detect", {
  method: "POST",
  body: JSON.stringify({ records, business_intent: "customer" }),
});

const groups = await api<{ group_id: string; record_ids: string[]; match_types: Record<string, string> }[]>(
  "/api/v1/duplicates/groups?resolved=false",
);
for (const g of groups) console.log(g.group_id, g.record_ids, g.match_types);

const scan = await api<{ message: string }>("/api/v1/recommendations/scan", {
  method: "POST",
  body: JSON.stringify({ provider: "postgresql" }),
});
console.log(scan.message);

const recs = await api<{ id: number; severity: string; integration_id: string; title: string }[]>(
  "/api/v1/recommendations?severity=high&status=active&limit=50",
);
for (const r of recs) console.log(r.id, r.severity, r.integration_id, r.title);