Every Verb, Every Role: The Airbyte HTTP Surface
One sentence in the controller sets the shape of everything below it: the vendor integration surface is **read-authority**. Import writes to the graph, and only additively. Audit and generate never write anything. Recording an audit run in the history is opt-in bookkeeping — except for re-audit, which always records its run because that is the whole point of the verb.
Every Verb, Every Role: The Airbyte HTTP Surface
One sentence in the controller sets the shape of everything below it: the vendor integration surface is read-authority. Import writes to the graph, and only additively. Audit and generate never write anything. Recording an audit run in the history is opt-in bookkeeping — except for re-audit, which always records its run because that is the whole point of the verb.
Read the route table with that in mind and the role column stops looking arbitrary. This article is the HTTP reference for governing an Airbyte estate: exact routes, exact request bodies, exact response fields, exact error strings — including the one verb this connector refuses. (The newer sync-plan routes appear in the table for completeness; their workflow is documented separately.)
Placeholders: https://coremodels.example.com is the API base URL, $TOKEN is your credential, {PROJECT_ID} is the 32-character hex project id.
Two surfaces
| Surface | Base | Auth | Verbs for airbyte |
|---|---|---|---|
| Interactive | graph/integrations/... | your CoreModels login token | vendors · import · audit · reaudit · history · badge · status · generate · reconcile · sync |
| Machine-to-machine | v1/... | user API keys | audit · badge |
The v1 surface exists so automation never has to hold an interactive login token. It carries audit and badge only; reaudit and history live on the interactive surface.
| Route | Method | Role |
|---|---|---|
graph/integrations/vendors | GET | any authenticated user |
graph/integrations/airbyte/import/{PROJECT_ID} | POST | Admin |
graph/integrations/airbyte/audit/{PROJECT_ID} | POST | Viewer |
graph/integrations/airbyte/reaudit/{PROJECT_ID} | POST | Viewer |
graph/integrations/airbyte/history/{PROJECT_ID} | GET | Viewer |
graph/integrations/airbyte/badge/{PROJECT_ID} | GET | Viewer |
graph/integrations/airbyte/status/{PROJECT_ID} | GET | Viewer |
graph/integrations/airbyte/generate/{PROJECT_ID} | POST | Viewer (refused — see below) |
graph/integrations/reconcile/{PROJECT_ID} | POST | Admin |
graph/integrations/airbyte/sync/propose/{PROJECT_ID} | POST | Viewer |
graph/integrations/sync/plan/{PROJECT_ID}/{PLAN_ID} | GET | Viewer |
graph/integrations/sync/ledger/{PROJECT_ID} | GET | Viewer |
v1/{PROJECT_ID}/integrations/airbyte/audit | POST | Viewer |
v1/{PROJECT_ID}/integrations/airbyte/badge | GET | Viewer |
The {vendor} path segment resolves through the connector registry case-insensitively, so airbyte, Airbyte and AIRBYTE all reach the same connector. The three sync-plan routes propose, fetch and list reviewable sync plans — sync/propose takes the same artifacts body as import and audit, and its only writes are bookkeeping (the stored plan and its ledger entry); that workflow has its own article.
The three request bodies
Import and audit share one body shape:
{
"artifacts": { "catalog": "<raw catalog.json content as a JSON string>" },
"spaces": [],
"recordHistory": false
}
artifacts(required) — artifact name to raw content. Airbyte has exactly one name:catalog.spaces(optional) — target space ids; empty or omitted means the project's main space.recordHistory(audit only, defaultfalse) — append this run to the rolling audit trail.
Re-audit takes no artifacts:
{ "projectName": null, "spaces": null, "notifySlack": false }
notifySlack (default false) additionally posts the recorded run to the project's configured Slack webhook when it carries errors or warnings. projectName selects which stored snapshot to re-audit; null means the most recent one. For Airbyte the stored estate is named airbyte-connection — a catalog carries no project name of its own, so the connector uses a stable estate name.
Reconciliation takes two vendor keys:
{ "vendorA": "airbyte", "vendorB": "snowflake", "spaces": null }
Discovery
curl -sS "https://coremodels.example.com/graph/integrations/vendors" \
-H "Authorization: Bearer $TOKEN" | jq '.vendors[] | select(.key == "airbyte")'
{
"key": "airbyte",
"displayName": "Airbyte",
"capabilities": "Import, Audit",
"artifacts": {
"catalog": "required — an AirbyteCatalog (source discover output) or ConfiguredAirbyteCatalog (connection export) JSON"
}
}
capabilities is a flags string rendered from what the connector declares. Read it before you write a client: it is the contract that decides which of the routes above will actually do work.
Import (Admin)
POST /graph/integrations/airbyte/import/{PROJECT_ID}
Authorization: Bearer $TOKEN
Content-Type: application/json
{ "artifacts": { "catalog": "<catalog.json>" } }
The response is a counter set plus two honesty channels:
| Field | Meaning |
|---|---|
datasetsAdded | streams written as new governed Types |
datasetsSkippedExisting | streams already governed — left untouched |
fieldsAdded | properties added to already-governed streams (the additive re-import pass) |
lineageEdgesAdded / lineageEdgesSkipped | lineage relations written / skipped |
nodesEnriched | nodes whose Airbyte metadata was written or refreshed |
snapshotStored | whether the parsed estate was persisted for artifact-free re-audit |
lossiness | what was approximated, each as {kind, path, explanation} |
errors | non-empty only when the import could not proceed |
Import is additive by design: already-governed streams are never mutated or deleted here. Changes to governed meaning surface through the audit and are applied by a human. The one exception is stated plainly in the layer's own posture: vendor-metadata mixin values are refreshed on every import, because they mirror the estate rather than carry governed meaning.
Audit (Viewer)
POST /graph/integrations/airbyte/audit/{PROJECT_ID}
Authorization: Bearer $TOKEN
Content-Type: application/json
{ "artifacts": { "catalog": "<catalog.json>" }, "recordHistory": true }
{
"success": true,
"vendor": "airbyte",
"projectName": "airbyte-connection",
"errorCount": 1, // > 0 ⇒ the change violates governed meaning
"warningCount": 1,
"infoCount": 2,
"codes": { "field-type-drift": 1, "stream-no-primary-key": 1, "no-cursor-field": 1, "untyped-fields": 1 },
"driftedObjects": ["public.users.created_at"],
"fingerprint": "9f2c41ab7d0e5b83",
"metrics": {
"datasets (estate)": "3",
"datasets governed": "3 / 3",
"fields governed": "11 / 11",
"governed nodes with canonical mappings": "0 / 15 (0%)",
"last import": "2026-08-04T09:12:44.1183920+00:00"
},
"findings": [
{
"section": "Drift",
"severity": "Error",
"code": "field-type-drift",
"subject": "public.users.created_at",
"message": "Field type changed since the last import.",
"detail": "governed: timestamp_with_timezone, estate: string"
}
],
"markdown": "# airbyte Schema Audit — airbyte-connection\n\n🔴 **1 errors · 1 warnings · 2 info**\n…",
"historyRecorded": true,
"lossiness": []
}
Findings are grouped into three sections with a fixed code vocabulary:
- Coverage —
dataset-unmapped(Warning),field-unmapped(Info),projection-unmapped(Info). - Drift —
dataset-removed,field-removed,field-type-drift,enum-narrowed(all Error),enum-constraint-removed,enum-widened(Warning),contract-drift. - Conformance — the connector's own rules. For Airbyte:
stream-no-primary-key(Warning),no-cursor-field(Info),untyped-fields(Info).
driftedObjects is the distinct subject list of the Drift section — the machine-readable answer to "what exactly moved". fingerprint is a content hash of the audited artifact, so two runs with the same fingerprint audited the same catalog.
Re-audit (Viewer)
Audit asks whether fresh artifacts still conform to the governed model. Re-audit asks the mirror question: does the governed model still match the last-known estate? It runs the identical audit engine over the snapshot stored at import time against the current governed view — no artifacts, no vendor calls.
curl -sS -X POST \
"https://coremodels.example.com/graph/integrations/airbyte/reaudit/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}' | jq '{errorCount, warningCount, codes, historyRecorded}'
The response shape is identical to audit, with historyRecorded: true always — this verb records its run with trigger reaudit. Additional metric: snapshot stored, the timestamp of the snapshot being re-audited.
Without a stored snapshot you get a plain error instead:
{
"success": false,
"payload": null,
"lossiness": [],
"errors": [
{
"path": "snapshot",
"message": "No stored estate snapshot for vendor 'airbyte' — import the vendor project first (imports persist the parsed snapshot)."
}
]
}
History, badge, status (Viewer)
{
"success": true,
"vendor": "airbyte",
"projects": [
{
"projectName": "airbyte-connection",
"runs": [
{
"at": "2026-08-04T09:31:07.4410000+00:00",
"trigger": "ci",
"errorCount": 0,
"warningCount": 1,
"infoCount": 2,
"codes": { "stream-no-primary-key": 1, "no-cursor-field": 1, "untyped-fields": 1 },
"fingerprint": "9f2c41ab7d0e5b83"
}
]
}
]
}
Runs are newest-first, one trail per vendor and estate, capped to the most recent fifty. The trigger vocabulary is audit, ci, reaudit, scheduled.
The badge route returns image/svg+xml — a shields-style badge labeled airbyte audit, colored from the latest recorded run: #4c1 clean, #dfb317 warnings only, #e05d44 with the error count, #9f9f9f when nothing has been recorded. An unknown vendor segment renders a gray badge rather than an error page, so a badge URL in a README is always safe.
Status reports the last import:
{
"success": true,
"vendor": "airbyte",
"imported": true,
"state": {
"vendor": "airbyte",
"projectName": "airbyte-connection",
"importedAt": "2026-08-04T09:12:44.1183920+00:00",
"toolVersion": null, // a catalog declares no tool version
"artifactVersion": null,
"generatedAt": null,
"sourceFingerprint": "9f2c41ab7d0e5b83",
"counts": "fieldsAdded=0, lineageAdded=0, lineageSkipped=0, nodesEnriched=14",
"facts": "{\"streams\":\"3\",\"fields\":\"11\"}"
},
"governedDatasets": 3
}
imported: false with a null state means nothing has been imported yet. governedDatasets counts vendor identities that currently resolve to a governed Type.
Cross-estate reconciliation (Admin)
When a second connector governs the same physical relations — the warehouse tables your Airbyte streams land in — reconciliation links the pairs as one governed entity with reciprocal sameAs assertions, dataset-level and field-level, idempotently:
curl -sS -X POST \
"https://coremodels.example.com/graph/integrations/reconcile/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"vendorA": "airbyte", "vendorB": "snowflake"}' \
| jq '{linksWritten, pairs, unmatchedA, unmatchedB}'
Matching is on the recorded physical relation name, normalized by stripping quotes, trimming and upper-casing — an exact string comparison, not a fuzzy one. An Airbyte stream's physical name is its namespace-qualified stream name, so pairs only match where that string equals the counterpart's recorded relation name. Everything unmatched is returned in unmatchedA / unmatchedB rather than guessed at.
The v1 machine surface
POST /v1/{PROJECT_ID}/integrations/airbyte/audit
Authorization: Bearer $TOKEN
Content-Type: application/json
{ "artifacts": { "catalog": "<catalog.json>" }, "recordHistory": true }
Same body, same fields — wrapped in the standard API envelope:
{
"success": true,
"error": null,
"data": { "vendor": "airbyte", "errorCount": 0, "warningCount": 1, "codes": { "stream-no-primary-key": 1 }, "markdown": "…" }
}
So automation gates on data.errorCount and prints data.markdown. With recordHistory: true the run is recorded with trigger ci. A failure comes back in the same envelope:
{ "success": false, "error": { "isFatal": false, "message": "…" }, "data": null }
The error catalog
Predictable strings you can assert against:
| Situation | Message |
|---|---|
| Unknown vendor segment | Unknown vendor '<v>'. Registered: <comma-joined keys>. |
Missing or empty artifacts | Body must include 'artifacts': { "<name>": "<content>" } (e.g. manifest for dbt). |
| Wrong artifact name | The 'catalog' artifact ((Configured)AirbyteCatalog JSON) is required. |
| Body is not JSON | Not valid JSON: <parser detail> |
| JSON without streams | Expected {"streams": [...]} with at least one stream. |
| Streams present, none named | No named streams found in the catalog. |
| Right shape, wrong internals | The catalog is valid JSON but not shaped like an Airbyte catalog: <detail> |
Generate: the refusal, in full
The generate route exists for every vendor segment. For Airbyte it returns:
{
"success": false,
"payload": null,
"lossiness": [],
"errors": [
{ "path": "capabilities", "message": "Connector 'airbyte' does not support generation." }
]
}
This is a design decision, not a missing feature. An Airbyte catalog states what a source exposes; generating one from the governed model would assert authorship over a system we do not control. The connector declares Import | Audit, the service refuses anything else at the capability gate, and nothing is silently emitted — the same contract that makes lossiness, snapshotStored and errorCount worth trusting.
For worked end-to-end calls against a real catalog, see the Airbyte quickstart in the CoreModels integration docs.