API Reference

Every HTTP route served by interlace serve. Interactive OpenAPI docs are always available on a running daemon at /schema/scalar (spec at /schema/openapi.json).

Conventions

  • Auth: Authorization: Bearer ilk_.... While no API key exists the whole API is open (keyless mode); the first key locks it down. Each route requires one scope — read, write, or admin; a key carries any combination, and an admin key satisfies every requirement. Missing/invalid token → 401; insufficient scope → 403.
  • Status codes: GETs and DELETEs return 200; POSTs return 201, except POST /runs/{id}/cancel and POST /environments/{name}/rollback, which return 200; errors are 400 (bad request/blocked), 401 (missing/invalid token), 403 (wrong scope), 404 (unknown), 429 (backpressure).
  • /health, /schema/*, and /ui/* never require auth.

Meta

RouteScopeDescription
GET /healthopen{status, version, environment}
GET /openRedirects to /ui/
GET /ui/...openThe web UI

Models & Lineage

RouteScopeDescription
GET /modelsreadAll models in topological order: name, materialise/output, strategy, fingerprint, depends_on, tags, schedule
GET /models/{name}readAdds full upstream/downstream closures, column lineage, canonical SQL (or Python source)
GET /models/{name}/impactreadColumn blast radius for ?column=COL: {source, impacted[{model, column, via}], opaque_consumers[]} — mirrors interlace impact. New in 1.0.2
GET /lineagereadThe whole graph in one payload: models, edges, column-level lineage, streams and their consumers — what the UI’s lineage canvas renders

Plan & Apply

RouteScopeDescription
GET /planreadQuery params environment, select (selector string), forward_only. Returns changes[] (with change_type, category, fingerprints, impacted columns, previous/new SQL) and transfers[]
POST /applywriteBody {selectors: [], environment, force: false, forward_only: false}. Breaking plan without force → 400. Returns {built, promoted, breaking, reused, transfers, rows, timings}

Runs

RouteScopeDescription
GET /runsreadDurable queue, newest first: state, attempts, error, partition window, idempotency key (its prefix names the trigger: cron:, interval:, api:, stream:)
GET /runs/{id}readRun detail plus its merged event history
POST /runswriteBody {selectors: [], environment, start, end, restate: false} (empty selectors = all models; ISO timestamps). Returns {enqueued, models}enqueued: 0 means deduplicated
POST /runs/{id}/cancelwrite200. Queued cancels now; running cancels at the worker’s next heartbeat. Unknown/finished → 404

Runs are executed by the scheduler loop with 60-second leases, up to 3 attempts, and cooperative cancellation.

Environments

RouteScopeDescription
GET /environmentsreadPer environment: promoted model count, drift vs the compiled project, promoted_at
DELETE /environments/{name}admin200. Drops views ({environment, dropped_views}); prod requires ?force=true; unknown → 404. Emits environment.dropped
GET /environments/{name}/historyreadPromotion generations, newest first — the rollback targets: [{generation, promoted_at, models}]
POST /environments/{name}/rollbackadmin200. Body {generation?} (default: the one before latest). Repoints views at that generation — nothing rebuilds. Emits environment.rolled_back

Checks

RouteScopeDescription
GET /checksreadRecorded check results (filter with ?model=)
POST /checks/runwriteBody {environment, selectors} (optional). Runs checks against promoted tables, no rebuild. Returns {outcomes, skipped, passed, blocking_failures}

Streams

RouteScopeDescription
GET /streamsreadPer stream: schema, table, head (accepted), watermark (materialised), pending (head − watermark), on_schema_drift, retention
GET /streams/{name}readAdds idempotency_key and recent (the last 20 payloads)
POST /streams/{name}writeBody: one JSON object or an array. Durable before it returns. {accepted, deduplicated, last_offset, quarantined}. Schema violations → 400 (reject/evolve) or quarantined (quarantine mode); warehouse too far behind → 429

Query Console

RouteScopeDescription
POST /queryreadBody {sql, limit: 500} (capped at 10,000). Exactly one SELECT/UNION statement — DDL/DML and external readers (read_csv, query, glob, HTTP/file) are rejected at parse. Runs on a sandboxed cursor with external access disabled (warehouse only), 30s timeout, ~8 MB cell cap. Returns {columns, types, rows, row_count, truncated, elapsed_ms}

System

RouteScopeDescription
GET /enginesreadConfigured engines (DSN credentials redacted)
GET /schedulesreadScheduled models: kind (cron/every), expression, next_fire, last_fired
POST /gcadminBody {grace: "7d", dry_run: false} (optional). Returns {removed_snapshots, dropped_tables, kept_snapshots, dry_run}

API Keys

RouteScopeDescription
GET /apikeysadminNames, scopes, creation times — never the secrets
POST /apikeysadminBody {name, scopes: ["read"]}. Returns the ilk_ token once
DELETE /apikeys/{name}admin200. Revokes every key with that name

Bootstrap: while keyless, POST /apikeys works unauthenticated — create the first (admin) key, then everything requires tokens.

Events

RouteScopeDescription
GET /eventsreadDurable event log; ?after=<seq> pages forward, 200 per call
GET /events/streamreadServer-Sent Events. Each message: event = type, id = sequence, data = the full event. Reconnects resume from the Last-Event-ID header with no gaps

Event types: run.enqueued, run.started, run.succeeded, run.retrying, run.failed, run.cancel_requested, run.cancelled, apply.started, apply.finished, apply.blocked, model.start, model.done, model.failed, model.cancelled, stream.flushed, environment.dropped, environment.rolled_back, gc.finished.

Note: browser EventSource can’t send an Authorization header — once keys exist, browser clients should poll GET /events; non-browser SSE clients pass the bearer header as usual.