# Microsoft Fabric — API: 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 route | Role | What it does |
|---|---|---|
| `GET graph/integrations/vendors` | any authenticated user | lists registered connectors, capabilities, artifact notes |
| `POST graph/integrations/fabric/import/{projectId}` | Admin | writes the estate into the graph, additively |
| `POST graph/integrations/fabric/audit/{projectId}` | Viewer | audits fresh artifacts against the governed model |
| `POST graph/integrations/fabric/reaudit/{projectId}` | Viewer | audits the stored estate snapshot against the *current* model |
| `GET graph/integrations/fabric/history/{projectId}` | Viewer | the rolling audit trail |
| `GET graph/integrations/fabric/badge/{projectId}` | Viewer | SVG status badge from the latest recorded run |
| `POST graph/integrations/fabric/generate/{projectId}` | Viewer | governed graph → T-SQL DDL |
| `GET graph/integrations/fabric/status/{projectId}` | Viewer | last-import state |
| `POST v1/{projectId}/integrations/fabric/audit` | Viewer | the same audit, for API keys |
| `GET v1/{projectId}/integrations/fabric/badge` | Viewer | the 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:

```bash
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:

```json
{
  "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.

```bash
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.

```json
{ "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:

```json
{
  "artifacts": { "information_schema": "<the extract, as a JSON string>" },
  "spaces": [],
  "recordHistory": true
}
```

```bash
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:

```bash
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
```

```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:

```bash
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:

```json
{ "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)

```bash
curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://coremodels.example.com/graph/integrations/fabric/history/$PROJECT_ID"
```

```json
{ "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.

```bash
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:

```bash
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.

```json
{ "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)

```bash
curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://coremodels.example.com/graph/integrations/fabric/status/$PROJECT_ID"
```

```json
{ "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:

```json
{ "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:

```json
{ "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.
