The Complete HTTP Surface of the CoreModels dbt Integration
This is the reference we wish every integration published: every route, every role, every request body, and every response shape for governing a dbt estate with CoreModels over plain HTTP. The dbt connector declares all three capabilities — Import, Audit, and Generate — so every verb below is live for `vendor = dbt`. (Not every connector can say that; the discovery route tells you per vendor, and a verb a connector doesn't support fails honestly rather than pretending.)
The Complete HTTP Surface of the CoreModels dbt Integration
This is the reference we wish every integration published: every route, every role, every request body, and every response shape for governing a dbt estate with CoreModels over plain HTTP. The dbt connector declares all three capabilities — Import, Audit, and Generate — so every verb below is live for vendor = dbt. (Not every connector can say that; the discovery route tells you per vendor, and a verb a connector doesn't support fails honestly rather than pretending.)
One posture sentence governs everything here: import and reconcile write to the graph additively; audit and generate never write anything. Recording an audit run in the history is opt-in bookkeeping, and the one exception — the reaudit verb — always records its run, because a re-audit exists precisely to extend the trail.
There are two surfaces:
- the interactive surface,
https://coremodels.example.com/graph/integrations/..., authenticated with your normal CoreModels login token; - the machine-to-machine surface,
https://coremodels.example.com/v1/..., which accepts user API keys — this is what CI calls.
All examples use $TOKEN and a 32-character hex {projectId}. Per-route project roles are shown on each verb.
Discovery
GET /graph/integrations/vendors
Authorization: Bearer $TOKEN
Any authenticated user. Returns each registered connector with key, displayName, capabilities, and artifacts — the artifact names it expects with a note per name. For dbt the notes read: manifest is required (target/manifest.json, produced by any dbt command), catalog is optional (from dbt docs generate, fills warehouse-real column types), semantic_manifest is optional.
An unknown {vendor} segment returns success: false with the message Unknown vendor '<v>'. Registered: <keys>. — the list of valid keys comes back with the error. The one exception is the badge route, which must return an image: it renders a gray unknown vendor badge instead.
Import — POST /graph/integrations/dbt/import/{projectId} (Admin)
POST /graph/integrations/dbt/import/{projectId}
Authorization: Bearer $TOKEN
Content-Type: application/json
{
"artifacts": {
"manifest": "<raw text of target/manifest.json>",
"catalog": "<raw text of target/catalog.json>"
},
"spaces": []
}
The body shape is ArtifactsRequest: artifacts maps artifact name to raw content and is required — an empty or missing map fails with Body must include 'artifacts': { "<name>": "<content>" } (e.g. manifest for dbt). spaces is optional; empty means the project's main space.
The response reports datasetsAdded, datasetsSkippedExisting, fieldsAdded, lineageEdgesAdded, lineageEdgesSkipped, nodesEnriched, snapshotStored, plus lossiness (approximations, honestly declared) and errors (could-not-proceed). Import never mutates existing governed nodes; a re-import adds what's new and skips the rest.
Audit — POST /graph/integrations/dbt/audit/{projectId} (Viewer)
Same ArtifactsRequest body, with one extra flag:
{
"artifacts": { "manifest": "<manifest.json>" },
"recordHistory": true
}
recordHistory defaults to false — the audit verb stays strictly read-only unless asked. The response is the full audit report:
{
"success": true,
"vendor": "dbt", "projectName": "jaffle_shop",
"errorCount": 0, // CI gate: > 0 ⇒ fail the build
"warningCount": 1, "infoCount": 2,
"codes": { "key-column-untested": 1, "source-no-freshness": 2 },
"driftedObjects": [],
"fingerprint": "<content hash of the artifacts>",
"metrics": { },
"findings": [ { "section": "Conformance", "severity": "Warning",
"code": "key-column-untested",
"subject": "model.jaffle_shop.orders.customer_id",
"message": "Key-shaped column has no tests (unique / not_null / relationships).",
"detail": null } ],
"markdown": "<PR-comment-ready report>",
"historyRecorded": true,
"lossiness": []
}
Findings fall into Coverage (dataset-unmapped, field-unmapped, projection-unmapped), Drift (dataset-removed, contract-drift, field-removed, field-type-drift, enum-constraint-removed, enum-narrowed, enum-widened), and Conformance — dbt's own rules: contract-not-enforced (Error for public models, Info otherwise), contract-column-missing-type (Error), key-column-untested (Warning), source-no-freshness (Info).
Reaudit — POST /graph/integrations/dbt/reaudit/{projectId} (Viewer)
POST /graph/integrations/dbt/reaudit/{projectId}
Authorization: Bearer $TOKEN
Content-Type: application/json
{}
The audit above asks "do these fresh artifacts still conform to the governed model?" Reaudit asks the reverse: "does the governed model still match the last-known estate?" It runs the same audit engine over the snapshot stored at import time against the current governed model — no artifacts needed. The optional body is { "projectName": null, "spaces": null, "notifySlack": false }, where projectName selects a vendor-side project (null means the latest stored snapshot) and notifySlack — opt-in, like recording — posts to the project's configured Slack channel when the run has errors or warnings. The run is always recorded in the history. If the import-time snapshot exceeded the storage cap (about 1.5 MB encoded), there is no stored snapshot and reaudit says so.
History — GET /graph/integrations/dbt/history/{projectId} (Viewer)
Returns the rolling audit trail, grouped by vendor-side project:
{ "success": true, "vendor": "dbt",
"projects": [ { "projectName": "jaffle_shop",
"runs": [ { "at": "2026-07-29T09:12:00.0000000+00:00", "trigger": "ci",
"errorCount": 0, "warningCount": 1, "infoCount": 2,
"codes": { "key-column-untested": 1 },
"fingerprint": "<hash>" } ] } ] }
Runs carry their trigger — an interactive audit with recordHistory (audit), a CI-surface audit (ci), a reaudit (reaudit), or the scheduled heartbeat (scheduled) — so drift over time is attributable.
Badge — GET /graph/integrations/dbt/badge/{projectId} (Viewer)
Returns image/svg+xml: a shields-style badge labeled dbt audit, rendered from the latest recorded run. Green means clean, yellow means warnings only, red means errors, gray means no recorded runs.
Generate — POST /graph/integrations/dbt/generate/{projectId} (Viewer)
The reverse direction: governed model out to dbt artifacts.
POST /graph/integrations/dbt/generate/{projectId}
Authorization: Bearer $TOKEN
Content-Type: application/json
{ "typeNames": [], "targetVersion": "1.8", "extra": {} }
typeNames restricts generation to named models (empty = everything eligible). targetVersion selects the dbt dialect: the default targets dbt ≥ 1.8 and emits the data_tests: key; "1.7" emits the legacy tests: key. extra is an open string-to-string bag for vendor-specific options.
The response returns generated files inline:
{ "success": true,
"artifacts": [ { "name": "models/coremodels_contracts.yml", "kind": "yaml",
"content": "<the schema.yml text>" } ],
"lossiness": [], "errors": [] }
For dbt that is one artifact — enforced model contracts with per-column data_type, not_null constraints, unique/not_null tests, accepted_values tests from governed taxonomies, and relationships tests from governed references. Generate is read-only: it computes the file and hands it to you; committing it to your repo is your reviewed change.
Status — GET /graph/integrations/dbt/status/{projectId} (Viewer)
{ "success": true, "vendor": "dbt", "imported": true,
"state": { "...": "versions, timestamps, fingerprint, counts" },
"governedDatasets": 9 }
The last-import state plus a count of vendor-identified datasets currently resolvable in the governed schema.
Reconcile and sync
Four more routes round out the interactive surface. Reconciliation is cross-vendor: when two connectors govern the same physical relation — say a dbt model and the warehouse table it materializes — one call links each pair as a single entity via reciprocal sameAs assertions. POST /graph/integrations/reconcile/{projectId} (Admin) takes { "vendorA": "dbt", "vendorB": "snowflake", "spaces": [] } and reports linksWritten, the matched pairs, and the unmatched remainder per side. The call is idempotent.
The sync-plan routes propose changes as a reviewable artifact rather than applying anything. POST /graph/integrations/dbt/sync/propose/{projectId} (Viewer) takes the same ArtifactsRequest body as audit and returns a full classified plan inline; it is read-only against governed meaning — its only writes are bookkeeping, the stored plan blob and a Proposed ledger entry that supersedes earlier proposals. GET /graph/integrations/sync/plan/{projectId}/{planId} fetches a stored plan for review, and GET /graph/integrations/sync/ledger/{projectId}?vendor=dbt lists the compact plan history, newest first (both Viewer).
The machine-to-machine surface (/v1)
Two routes live here, both Viewer role, both accepting user API keys:
POST /v1/{projectId}/integrations/dbt/audit
GET /v1/{projectId}/integrations/dbt/badge
The audit takes the same ArtifactsRequest body and returns the same report — but wrapped in the standard API envelope, so counts live under data.*: your CI script reads data.errorCount, and data.errorCount > 0 means the pull request violates governed meaning — fail the build. When recordHistory is true here, the run is recorded with trigger ci. The badge is the same SVG, embeddable in READMEs with a user API key.
Deliberately not on this surface: reaudit and history live on the interactive surface only; the v1 API-key surface carries audit and badge. A leaked CI key can read a report and a badge — it cannot enumerate your drift history.
Role summary
| Verb | Route | Role |
|---|---|---|
| Discovery | GET /graph/integrations/vendors | any authenticated |
| Import | POST /graph/integrations/dbt/import/{projectId} | Admin |
| Audit | POST /graph/integrations/dbt/audit/{projectId} | Viewer |
| Reaudit | POST /graph/integrations/dbt/reaudit/{projectId} | Viewer |
| History | GET /graph/integrations/dbt/history/{projectId} | Viewer |
| Badge | GET /graph/integrations/dbt/badge/{projectId} | Viewer |
| Generate | POST /graph/integrations/dbt/generate/{projectId} | Viewer |
| Status | GET /graph/integrations/dbt/status/{projectId} | Viewer |
| Reconcile | POST /graph/integrations/reconcile/{projectId} | Admin |
| Sync propose | POST /graph/integrations/dbt/sync/propose/{projectId} | Viewer |
| Sync plan | GET /graph/integrations/sync/plan/{projectId}/{planId} | Viewer |
| Sync ledger | GET /graph/integrations/sync/ledger/{projectId} | Viewer |
| CI audit | POST /v1/{projectId}/integrations/dbt/audit | Viewer (API key) |
| CI badge | GET /v1/{projectId}/integrations/dbt/badge | Viewer (API key) |
Only import and reconcile need Admin, because only they write governed content. Everything that reads — including generate, which computes but does not store — runs at Viewer.
A worked end-to-end walkthrough of these calls lives in the dbt quickstart that ships with CoreModels.