Microsoft Fabric logoAPI

The Fabric Integration API: Every Route, Every Role, Every Payload

CoreModels exposes vendor integrations over two HTTP surfaces, and the split is not decoration. The interactive surface under `graph/integrations/...` is what a person or a notebook calls with a normal CoreModels login token. The machine-to-machine surface under `v1/...` accepts user API keys and is what a build pipeline calls. Both require authentication; both then enforce a per-project role on top of it.

The Fabric Integration API: Every Route, Every Role, Every Payload

CoreModels exposes vendor integrations over two HTTP surfaces, and the split is not decoration. The interactive surface under graph/integrations/... is what a person or a notebook calls with a normal CoreModels login token. The machine-to-machine surface under v1/... accepts user API keys and is what a build pipeline calls. Both require authentication; both then enforce a per-project role on top of it.

This is the core Microsoft Fabric surface — the import, audit, and drift loop. (The vendor-neutral cross-estate routes, reconciliation and sync-plan proposal, serve Fabric too but are shared across connectors.) The connector key is fabric, and the same routes serve plain SQL Server estates, because the artifact contract is ANSI T-SQL INFORMATION_SCHEMA. Everything below uses https://coremodels.example.com as the API base and $PROJECT_ID as the 32-character hex id of the governing project.

Verb and routeRoleWhat it does
GET graph/integrations/vendorsany authenticated userlists registered connectors, capabilities, artifact notes
POST graph/integrations/fabric/import/{projectId}Adminwrites the estate into the graph, additively
POST graph/integrations/fabric/audit/{projectId}Vieweraudits fresh artifacts against the governed model
POST graph/integrations/fabric/reaudit/{projectId}Vieweraudits the stored estate snapshot against the current model
GET graph/integrations/fabric/history/{projectId}Viewerthe rolling audit trail
GET graph/integrations/fabric/badge/{projectId}ViewerSVG status badge from the latest recorded run
POST graph/integrations/fabric/generate/{projectId}Viewergoverned graph → T-SQL DDL
GET graph/integrations/fabric/status/{projectId}Viewerlast-import state
POST v1/{projectId}/integrations/fabric/auditViewerthe same audit, for API keys
GET v1/{projectId}/integrations/fabric/badgeViewerthe same badge, for API keys

Note what is deliberately not symmetric: reaudit and history live on the interactive surface only. The API-key surface carries audit and badge.

Discovery

Before hardcoding anything, ask the server what it has:

curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://coremodels.example.com/graph/integrations/vendors"

The Fabric entry answers the two questions that matter — what it can do, and what to feed it:

{
  "key": "fabric",
  "displayName": "Microsoft Fabric",
  "capabilities": "Import, Audit, Generate",
  "artifacts": {
    "information_schema": "required — JSON rows of T-SQL INFORMATION_SCHEMA COLUMNS × TABLES (documented query; SQL Server works too)",
    "keys": "optional — flattened PK/FK constraint rows (documented query)"
  }
}

Import (Admin)

Import is the only verb that writes governed structure, and it writes additively: already-governed datasets are never mutated or deleted. Vendor metadata values are refreshed on every import, because they mirror the estate rather than carry governed meaning.

jq -n --rawfile info information_schema.json --rawfile keys keys.json \
  '{artifacts: {information_schema: $info, keys: $keys}, spaces: []}' > import-request.json

curl -sS -X POST \
  "https://coremodels.example.com/graph/integrations/fabric/import/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @import-request.json

spaces is optional on every artifact-bearing route; an empty array or an omitted field targets the project's main space.

{ "success": true, "vendor": "fabric", "projectName": "wh_sales",
  "datasetsAdded": 12, "datasetsSkippedExisting": 0, "fieldsAdded": 96,
  "lineageEdgesAdded": 0, "lineageEdgesSkipped": 0, "nodesEnriched": 108,
  "snapshotStored": true, "lossiness": [], "errors": [] }

snapshotStored is the field to watch in automation. Import persists the parsed estate so later re-audits need no artifacts; when the encoded snapshot exceeds the storage cap it comes back false with a lossiness record explaining exactly that, and reaudit will have nothing to run against.

Audit (Viewer)

The audit body is the import body plus one flag:

{
  "artifacts": { "information_schema": "<the extract, as a JSON string>" },
  "spaces": [],
  "recordHistory": true
}
curl -sS -X POST \
  "https://coremodels.example.com/graph/integrations/fabric/audit/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @audit-request.json

recordHistory defaults to false: the audit verb stays strictly read-only unless you ask it to record. The response carries errorCount, warningCount, infoCount, a codes map of finding code to occurrence count, driftedObjects, the artifact fingerprint, a metrics table, the full findings array, a pull-request-ready markdown rendering, historyRecorded, and lossiness.

The same audit through an API key returns the identical body wrapped in the standard envelope, which is the one difference pipeline code has to handle:

curl -sS -X POST \
  "https://coremodels.example.com/v1/$PROJECT_ID/integrations/fabric/audit" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @audit-request.json
{ "success": true, "error": null,
  "data": { "vendor": "fabric", "projectName": "wh_sales",
            "errorCount": 0, "warningCount": 2, "infoCount": 1,
            "codes": { "key-column-undeclared": 2, "descriptions-not-extracted": 1 },
            "driftedObjects": [], "fingerprint": "9f3c1a72b48d05e6",
            "historyRecorded": true } }

Counts live under data.* here. A recorded run from this surface is stamped with the trigger ci; from the interactive surface, audit.

Re-audit (Viewer)

Re-audit asks the mirror-image question. The audit asks whether fresh artifacts still conform to the governed model; re-audit asks whether the last-known estate still conforms to a model that people have since changed. It runs the identical engine over the stored snapshot, needs no artifacts and no credentials, and always records its run:

curl -sS -X POST \
  "https://coremodels.example.com/graph/integrations/fabric/reaudit/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "projectName": null, "spaces": null }'

projectName selects which stored estate to replay when one CoreModels project governs several warehouses; null takes the most recently stored one. The report gains one extra metric, Snapshot stored, carrying the timestamp of the import it replays. With nothing stored you get a precise refusal:

{ "success": false, "payload": null, "lossiness": [],
  "errors": [ { "path": "snapshot",
    "message": "No stored estate snapshot for vendor 'fabric' — import the vendor project first (imports persist the parsed snapshot)." } ] }

History and badge (Viewer)

curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://coremodels.example.com/graph/integrations/fabric/history/$PROJECT_ID"
{ "success": true, "vendor": "fabric",
  "projects": [
    { "projectName": "wh_sales",
      "runs": [
        { "at": "2026-02-14T06:00:11.9032104+00:00", "trigger": "scheduled",
          "errorCount": 1, "warningCount": 2, "infoCount": 1,
          "codes": { "field-type-drift": 1, "key-column-undeclared": 2,
                     "descriptions-not-extracted": 1 },
          "fingerprint": "9f3c1a72b48d05e6" },
        { "at": "2026-02-11T09:31:52.1180000+00:00", "trigger": "ci",
          "errorCount": 0, "warningCount": 2, "infoCount": 1,
          "codes": { "key-column-undeclared": 2, "descriptions-not-extracted": 1 },
          "fingerprint": "9f3c1a72b48d05e6" }
      ] }
  ] }

Runs are compact by design — counts, codes and fingerprint, never the full findings. The trail answers "is this estate drifting over time"; the live audit answers "what exactly is wrong right now". Trigger values are audit, ci, reaudit and scheduled.

The badge route returns image/svg+xml rendered from the latest recorded run: green when clean, yellow when only warnings, red with the error count, gray when nothing has been recorded yet.

curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://coremodels.example.com/v1/$PROJECT_ID/integrations/fabric/badge" > fabric-audit.svg

Generate (Viewer)

Fabric declares the Generate capability, so the loop closes both ways. Generation reads the governed graph and emits T-SQL:

curl -sS -X POST \
  "https://coremodels.example.com/graph/integrations/fabric/generate/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "typeNames": [], "spaces": null }'

An empty typeNames means everything eligible; entries filter by governed type label or by the recorded physical relation name. The shared request body also accepts targetVersion and extra, which other connectors use for dialect switches — the Fabric generator reads neither, so omitting them changes nothing.

{ "success": true,
  "artifacts": [
    { "name": "coremodels_fabric_tables.sql", "kind": "sql",
      "content": "-- Generated by CoreModels — governed T-SQL table definitions (Fabric Warehouse / SQL Server).\n-- Meaning changes belong in CoreModels; regenerate this script rather than editing it.\n\nCREATE TABLE wh_sales.dbo.customers (\n    [customer_id] bigint NOT NULL,\n    [full_name] nvarchar(200) NOT NULL,\n    [signup_date] datetime2 NULL,\n    PRIMARY KEY ([customer_id])\n);\n" }
  ],
  "lossiness": [
    { "kind": "StructuralDrop", "path": "orders_enriched",
      "explanation": "Views are derived objects; DDL generation covers tables only." }
  ],
  "errors": [] }

Identifiers are bracketed, NOT NULL and NULL come from the recorded checks, each table gets at most one PRIMARY KEY, and governed references become FOREIGN KEY ... REFERENCES declarations. Views are skipped with a declared lossiness record rather than silently. If nothing is eligible at all, the call fails cleanly with { "path": "generate", "message": "No eligible tables found to generate DDL for." }.

Status (Viewer)

curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://coremodels.example.com/graph/integrations/fabric/status/$PROJECT_ID"
{ "success": true, "vendor": "fabric", "imported": true,
  "state": { "vendor": "fabric", "projectName": "wh_sales",
             "importedAt": "2026-02-11T09:14:22.4180000+00:00",
             "toolVersion": null, "artifactVersion": null, "generatedAt": null,
             "sourceFingerprint": "9f3c1a72b48d05e6",
             "counts": "fieldsAdded=0, lineageAdded=0, lineageSkipped=0, nodesEnriched=108",
             "facts": "{\"tables\":\"10\",\"views\":\"2\",\"descriptionsAvailable\":\"false\"}" },
  "governedDatasets": 12 }

The null version fields are honest rather than decorative: an INFORMATION_SCHEMA extract carries no tool stamp and no artifact schema version, so nothing is claimed for them. The facts string is what the parser counted — tables, views, and the estate-level note that descriptions were not available.

Failure shapes

Two mistakes account for most first-call failures, and both answer with a structured error rather than a stack trace.

An unrecognized vendor segment lists what is actually registered:

{ "success": false, "lossiness": [],
  "errors": [ { "path": "vendor",
    "message": "Unknown vendor 'fabrik'. Registered: ..." } ] }

The real message enumerates every connector key the server has registered, sorted alphabetically — paste the right one into your config and move on.

An empty or artifact-less body answers:

{ "success": false, "lossiness": [],
  "errors": [ { "path": "artifacts",
    "message": "Body must include 'artifacts': { \"<name>\": \"<content>\" } (e.g. manifest for dbt)." } ] }

The example inside that message is dbt's, because the body validation is shared across all connectors. For Fabric the required artifact name is information_schema, with keys optional.

One posture note to close on, because it explains the whole role column above. We call this surface read-authority: import writes, and only additively. Audit, re-audit, generate, history, badge and status never touch governed meaning — recording an audit run is bookkeeping, and always something the caller explicitly asked for. That is why a build pipeline never needs more than a Viewer-scoped key.

The two extraction queries these routes expect are documented in the Microsoft Fabric quickstart.