Appearance
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.
| Method | Path | Purpose | Needs |
|---|---|---|---|
| POST | /api/v1/mapping/map | Map one vendor record to the target model and persist the result | write |
| POST | /api/v1/mapping/suggest | Ask the mapping agent for the best target column for one source field | read |
| GET | /api/v1/mapping/saved | List stored mappings, newest first | read |
| GET | /api/v1/mapping/saved/{filename} | Fetch one stored mapping | read |
| POST | /api/v1/mapping/feedback | Record operator feedback on a mapping | write |
| GET | /api/v1/mapping/feedback | List recorded feedback rows | read |
| GET | /api/v1/mapping/feedback/metrics | Accuracy metrics and volume counts over feedback | read |
| POST | /api/v1/lineage/edge | Create or update one lineage edge (and its endpoint nodes) | write |
| GET | /api/v1/lineage/graph | Return the whole lineage graph | read |
| GET | /api/v1/lineage/impact/{node_key} | Downstream blast radius of one node | read |
| POST | /api/v1/lineage/sync | Start a background lineage sync, returns a job id | write |
| POST | /api/v1/lineage/sync/now | Run a lineage sync and wait for the result | write |
| POST | /api/v1/semantic/extract-intent | Extract business intent for one field name | write |
| GET | /api/v1/semantic/mappings-by-intent/{intent} | Semantic mappings that share one business intent | read |
| POST | /api/v1/semantic/find-semantic-match | Find an existing mapping that matches a source field to target fields | read |
| GET | /api/v1/semantic/mapping-history/{mapping_id} | Version history of one semantic mapping | read |
| GET | /api/v1/semantic/intent-registry/stats | Counts from the intent registry | read |
| GET | /api/v1/semantic/intent-registry/signatures | Signatures held in the intent registry | read |
| GET | /api/v1/semantic/stats | Explorer stats computed from persisted semantic mappings | read |
| GET | /api/v1/semantic/signatures | Explorer signature rows computed from persisted mappings | read |
| GET | /api/v1/semantic/signatures/{signature_hash}/matches | Systems that contain fields for one signature | read |
| GET | /api/v1/duplicates/config | Read the duplicate handling mode | read |
| PUT | /api/v1/duplicates/config | Set the duplicate handling mode | write |
| POST | /api/v1/duplicates/validate-before-write | Gate a record before it is written to a target | write |
| GET | /api/v1/duplicates/groups | List known duplicate groups | read |
| POST | /api/v1/duplicates/detect | Detect duplicate groups in a batch of records | write |
| GET | /api/v1/duplicates/{record_id} | Duplicate group that contains one record | read |
| POST | /api/v1/duplicates/resolve | Record a resolution for a duplicate group | write |
| GET | /api/v1/duplicates/resolutions/history | Resolution history | read |
| POST | /api/v1/duplicates/check | Check one record against existing records | write |
| GET | /api/v1/recommendations | List estate advisor recommendations | read |
| POST | /api/v1/recommendations/scan | Run the advisor scan | write |
| PATCH | /api/v1/recommendations/{recommendation_id} | Dismiss, resolve or reactivate a recommendation | write |
| GET | /api/v1/reconciliation/pairings | List reconciliation pairings | read |
| GET | /api/v1/reconciliation/pairings/{pairing_id} | One pairing with per-partition state | read |
| POST | /api/v1/reconciliation/pairings | Declare a pairing in proposed status | write |
| PATCH | /api/v1/reconciliation/pairings/{pairing_id} | Edit a proposed or paused pairing | write |
| POST | /api/v1/reconciliation/pairings/{pairing_id}/activate | Activate a pairing | write |
| POST | /api/v1/reconciliation/pairings/{pairing_id}/pause | Pause an active pairing | write |
| POST | /api/v1/reconciliation/pairings/{pairing_id}/reject | Reject a proposed or paused pairing | write |
| GET | /api/v1/reconciliation/findings | List reconciliation findings | read |
| POST | /api/v1/analysis/chat | Ask a question about the deployment | read |
| POST | /api/v1/analysis/chat/stream | Same as chat, streamed as server-sent events | read |
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 type | Key pattern | Meaning |
|---|---|---|
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_source | Meaning |
|---|---|
exact | A real count of the table. |
catalog_estimate | The database's own statistic (for example pg_class.reltuples or information_schema.TABLES.TABLE_ROWS). It can drift between statistics refreshes. |
unknown | No 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.
| Field | Type | Meaning |
|---|---|---|
mapping_id | string (UUID) | Primary key. |
source_vendor | string | Identifier of the source system, normally an integration id. |
source_schema, source_table, source_field | string or null, null, string | Source location. |
target_model, target_schema, target_table, target_field | string, null, null, string | Target location. |
business_intent | string or null | Concept name such as Customer Name. This is what mappings-by-intent matches, exactly. |
business_intent_category | string or null | One of customer, order, product, payment, address, contact, identifier, date_time, amount, status, code, description, metadata, other. |
semantic_signature | string or null | Stable signature of the intent. |
concept | string or null | Concept label, used when business_intent is empty. |
mapping_type | string | direct, transform, split, merge and so on. |
transformation_type, transformation_expression, transformation_parameters | string, string, object (all nullable) | Transformation detail. |
confidence_score | number | Required, defaults to 0.0. |
similarity_score, semantic_equivalence_score | number or null | Optional scores. |
organization_id | string | Single-tenant deployment id. It is derived on the server and never supplied by the caller. |
field_description, sample_values, notes | string, array, string (nullable) | Context. |
created_at, updated_at, created_by | ISO 8601 string, ISO 8601 string, string | Audit 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.
| From | Allowed next status |
|---|---|
proposed | active, rejected |
active | paused |
paused | active, rejected |
rejected | none |
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:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
target_model | string | no | UniversalCustomer | Target 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):
| Name | Type | Required | Description |
|---|---|---|---|
vendorId | string | yes | Vendor identifier. Becomes sourceVendor in the result. |
vendorName | string | yes | Display name. |
objectType | string | yes | Kind of record, for example customer. |
rawData | object | yes | The record. Column names are taken from its keys. |
fieldMappings | array or null | no | Hints. Each item has vendorField, targetField and optional transformationType (direct, computed, lookup, custom). Default null. |
metadata.recordId | string | yes | Vendor record id. |
metadata.recordType | string | no | Record type. |
metadata.lastModified | ISO 8601 datetime | yes | Last modified time. |
metadata.apiVersion | string | no | Vendor API version. |
metadata.retrievedAt | ISO 8601 datetime | yes | When the record was fetched. |
Response 200 (MappingResult, camelCase):
| Field | Type | Description |
|---|---|---|
mappingId | string | Result id. |
sourceVendor | string | Echo of vendorId. |
targetModel | string | Echo of target_model. |
mappedData | object | Target column name to source value. |
fieldMappings | array | Items with sourceField, targetField, sourceValue, mappedValue, confidence, transformationType (direct, computed, inferred, custom) and optional aiReasoning. |
isValid | boolean | True when the agent returned no errors. |
validationErrors, validationWarnings | array | Items with field, errorType (missing_required, invalid_format, out_of_range, type_mismatch, custom), message, severity (error, warning, info). |
aiModel, processingTime | string, number | Optional. |
metadata | object | mappedAt, 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.
| Name | Type | Required | Description |
|---|---|---|---|
source_field | string | yes | Source column name. |
target_candidates | array of string | yes | Candidate 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.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
filename | path | string | yes | A 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.
| Name | Type | Required | Description |
|---|---|---|---|
mapping_id | string | yes | Mapping the feedback is about. Minimum length 1. |
source_field | string | yes | Minimum length 1. |
target_field | string | yes | Minimum length 1. |
feedback_type | string | yes | correct, incorrect, partial or manual_override (case-insensitive, trimmed). |
ai_suggested_field | string | no | Defaults to target_field when omitted. |
user_corrected_field | string | no | The field the operator chose instead. |
user_comment | string | no | Free text. |
ai_confidence | number | no | Confidence the AI reported. |
ai_reasoning | string | no | The AI's stated reason. |
source_table, target_table | string | no | Tables for the two fields. |
source_vendor, target_model | string | no | Context. |
project_id | string | no | Project scope. |
sentiment | string | no | Free text label. |
data_type | string | no | Column type. |
transformation_applied | string | no | Transformation that was used. |
session_id | string | no | Session 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).
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
mapping_id | query | string | no | none | Filter. |
project_id | query | string | no | none | Filter. |
feedback_type | query | string | no | none | Filter. Validated against the four values above (422 otherwise). |
limit | query | integer | no | 100 | Range 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.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
project_id | query | string | no | none | Restrict to one project. |
days | query | integer | no | 30 | Window for the accuracy block, range 1 to 365. |
Response 200:
| Field | Type | Description |
|---|---|---|
accuracy | object | total_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). |
volume | object | mapping_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, days | string or null, integer | Echo 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.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
source | string | yes | none | Source node_key. |
target | string | yes | none | Target node_key. |
edge_type | string | no | mapping | Edge label. Edges are unique on (source, target, edge_type). |
metadata | object | no | null | Attributes 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:
| Field | Type | Description |
|---|---|---|
nodes[].id | string | The node_key. |
nodes[].label | string | Provider display name for source and endpoint nodes, otherwise the last dot-separated segment of the key. |
nodes[].type | string | Node type. |
nodes[].provider, .host, .schema | string | Hoisted from attributes, empty string when absent. |
nodes[].metadata | object or null | Full 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[].quarantined | boolean | Containment flag. |
edges[].id | string | {source}-{target}. |
edges[].source, .target | string | Node keys. |
edges[].label | string | Edge type. |
edges[].metadata | object or null | Edge 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.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
node_key | path | string | yes | none | Node to start from. |
max_depth | query | integer | no | 50 | Clamped to the range 1 to 200 by the server. |
Response 200:
| Field | Type | Description |
|---|---|---|
origin | string | The requested key. |
origin_quarantined | boolean | Containment flag for the origin. |
affected_nodes[] | array | id, label, type, provider, host, schema, depth (hops from origin), quarantined. A key with no stored node gets type of unknown. |
edges[] | array | The edges walked: id, source, target, label. |
truncated | boolean | True 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.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
include_unconfigured | boolean | no | true | Also 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_credentials | object or null | no | null | Map 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:
| Field | Type | Description |
|---|---|---|
nodes_created | integer | Nodes written. |
edges_created | integer | Edges written. |
integrations_processed, integrations_succeeded, integrations_failed | integer | Counts over discovered database integrations. |
errors | array of string | One 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.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
field_name | string | yes | none | Field to analyze. |
field_description | string | no | null | Context. |
sample_values | array | no | null | Example values. |
data_type | string | no | null | Column type. |
use_llm | boolean | no | true | Allow 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.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
intent | path | string | yes | none | Concept name, URL-encoded (for example Customer%20Name). |
limit | query | integer | no | 100 | Maximum 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.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
source_field | string | yes | none | Source field name. |
target_fields | array of string | yes | none | Candidate target names. |
source_vendor | string | no | null | Narrow to one source system. |
target_model | string | no | null | Narrow to one target model. |
min_confidence | number | no | 0.5 | Minimum 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.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
category | query | string | no | none | One of the category values in the semantic mapping table. |
limit | query | integer | no | 100 | Applied 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:
| Field | Type | Description |
|---|---|---|
signature_hash | string | Echo. |
matches[] | array | One item per source system, sorted by number of matched fields descending, then name. |
matches[].system_id | string | The mapping's source_vendor, or unknown. |
matches[].system_name, .provider | string | Display name and provider of the system, falling back to system_id. |
matches[].matched_fields | array of string | table.field (or field when there is no table), de-duplicated and sorted. |
count | integer | Number 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.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
mode | string | no | null | flag 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.
| Name | Type | Required | Description |
|---|---|---|---|
record | object | yes | The record about to be written. |
existing_records | array of object | no | Records to compare against. |
business_intent | string | no | Category 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.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
resolved | query | boolean | no | none | True 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.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
records | array of object | yes | none | Records to compare, pairwise. |
business_intent | string | no | null | Category 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.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
group_id | string | yes | none | Group to resolve. |
primary_record_id | string | yes | none | Record to keep. |
action | string | yes | none | merged, kept_separate, deleted, ignored or pending (case-insensitive). |
merged_record_ids | array of string | no | [] | Records folded into the primary. |
deleted_record_ids | array of string | no | [] | Records marked deleted. |
resolved_by | string | no | null | Free text name of the person. |
notes | string | no | null | Free 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.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
group_id | query | string | no | none | Restrict to one group. |
limit | query | integer | no | 100 | Maximum 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.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
integration_id | query | string | no | none | Filter. |
provider | query | string | no | none | Filter. |
recommendation_type | query | string | no | none | Matches either recommendation_type or category. |
category | query | string | no | none | Legacy alias, used only when recommendation_type is absent. |
severity | query | string | no | none | Filter. |
status | query | string | no | active | When omitted, only active rows are returned. |
limit | query | integer | no | 200 | Maximum 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.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
integration_id | string | no | null | Scan one integration. Takes precedence over provider. |
provider | string | no | null | Scan every integration of one provider. |
analyzer_types | array of string | no | null | Restrict 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.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
recommendation_id | path | integer | yes | Recommendation id. |
status | body | string | yes | active, 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.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
status | query | string | no | none | proposed, 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 field | Type | Description |
|---|---|---|
partition_ref | integer | State id. It equals partition_ref on findings. |
partition_value | string | The literal partition value. |
a_row_count, b_row_count, b_key_count | integer or null | Row counts per side. b_key_count counts distinct keys on B. |
a_sum_minor, b_sum_minor | integer or null | Measure sums in minor units. |
a_last_activity_at | string or null | Latest A-side timestamp seen. |
a_age_seconds | integer or null | Age of that timestamp at the last check. |
settled | boolean | True once the A-side age reaches the settle window. |
agrees | boolean or null | Result of the last comparison. |
consecutive_disagreements | integer | Consecutive disagreeing checks. |
first_disagreed_at, last_checked_at | string or null | Timestamps. |
open_finding_event_id | integer or null | Open 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.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | yes | none | 1 to 200 characters. |
a_integration_id | string | yes | none | Integration id of side A (the source of truth, for example billing). |
b_integration_id | string | yes | none | Integration 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_table | string | yes | none | Table names. |
a_key_column, b_key_column | string | yes | none | Join key columns. A is expected to be unique per key. B may repeat a key and is summed. |
a_measure_column, b_measure_column | string | yes | none | Amount columns. |
a_database, b_database | string | yes | none | Database names. Omitting one returns 422 missing_database. |
a_schema, b_schema | string | no | null | Schema names. |
a_measure_scale, b_measure_scale | integer | no | null | Required at activation, 0 to 6. |
minor_unit_exponent | integer | no | 2 | Decimal places used to format *_display strings. |
currency | string | no | null | One currency per pairing. No conversion is done. |
a_partition_column, b_partition_column | string | no | null | Required at activation. Must not equal the key column. |
partition_value_type | string | no | string | string or integer. |
a_timestamp_column | string | no | null | Required at activation. Latest activity on side A. |
a_timestamp_kind | string | no | utc | utc or source_local. |
settle_window_seconds | integer | no | null | Required at activation, 0 or more. How old the newest A-side activity must be before a partition counts as settled. |
cadence_seconds | integer | no | 900 | 60 or more. |
tolerance_per_partition_minor | integer | no | 0 | Absolute tolerance on a partition's sum, in minor units. 0 or more. |
tolerance_per_key_minor | integer | no | 0 | Absolute tolerance per key, in minor units. 0 or more. |
lookback_partitions | integer | no | 10 | 1 to 50. |
lookback_days | integer | no | 35 | 1 to 366. |
key_normalizer | string | no | null | strip_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.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
pairing_id | query | string | no | none | Restrict to one pairing. |
include_resolved | query | boolean | no | false | Include findings that later checks cleared. |
limit | query | integer | no | 100 | Range 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.
| Field | Type | Description |
|---|---|---|
id | integer | Detection event id. |
integration_id | string | The B-side integration. |
detected_at, resolved_at | string, string or null | Timestamps. |
impact_estimate | any or null | Impact estimate when one was computed. |
pairing_id, pairing_name | string | The pairing. |
a_integration_id, b_integration_id | string | Both sides. |
a_table, b_table | string | Qualified database.schema.table, omitting empty parts. |
partition_column_a, partition_column_b | string | Partition columns. |
partition_ref | integer | Matches partition_ref in the pairing's partitions. |
partition | string | The literal partition value. |
lookup_sql | object | {"a": "<read-only SQL>", "b": "<read-only SQL>"} for the operator to run in their own tooling. |
comparison | string | aggregate, key_level or fingerprint. |
a_count, b_count, b_row_count, count_delta | integer | A key count, B distinct key count, B row count and the count difference. |
a_sum_minor, b_sum_minor, sum_delta_minor | integer | Sums and difference in minor units. |
minor_unit_exponent, currency | integer, string or null | Formatting hints. |
sum_delta_display, measured_amount_minor, measured_amount_display | string, integer, string | Formatted difference and its absolute value. |
settle_window_seconds, a_age_seconds | integer | Settle window and the A-side age at detection. |
detail | string | Human-readable summary. |
row_count | integer | Absolute 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:
| Name | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | Project identifier. |
question | string | yes | The question. |
history | array of object | no | Earlier turns as {"role": "user", "content": "..."}. role defaults to user. |
POST /api/v1/analysis/chat
Needs: read. Response 200:
| Field | Type | Description |
|---|---|---|
answer | string | Always populated. |
blocks | array of object | Structured payloads such as tables. Empty for plain answers. |
console_links | array | Items with label, route, tab (nullable) and focus_node_id (nullable, set when a tool resolved a specific lineage node). |
path | string | fast (deterministic, no model) or slow (model path). |
used_tools | array of string | Tools 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);