Skip to main content

API reference

PLACEHOLDER Cloud exposes a versioned REST API over HTTP. The authoritative shape is the OpenAPI schema you can dump at any time with dqc export-schema. This page is the entry-level walkthrough — the surface, the auth, the conventions.

Base URL

https://placeholder.example.com/api in production. http://localhost:8000/api locally.

API versioning

There are two routing prefixes today:

PrefixUse
/api/v1/…The original stable surface. Most read endpoints and AI / alert / schedule / agent-token resources live here.
/api/v2/…The newer routes that introduce org-scoped + workspace-scoped paths. Use these for new integrations. The pattern is /api/v2/organizations/{org_id}/workspaces/{ws_id}/....

Both versions sit behind the same auth flow. No version is deprecated yet.

Authentication

Three token types — never confuse them.

TokenWhere it comes fromHeader
JWTPOST /auth/login returns one as access_token. Lifetime: 15 min.Authorization: Bearer <jwt>
dqk_… (personal API key)User generates one in Settings → API keys.Authorization: Bearer dqk_…
dqa_… (agent token)Owner generates one in Settings → Agents. Scoped to one org + (optional) workspace.Authorization: Bearer dqa_…

JWT refresh: POST /auth/refresh exchanges a dqr_… refresh-token cookie for a fresh JWT.

For non-interactive callers (CI, scripts, the SDKs), a personal API key is the right token: mint one in Settings → API keys, send it as Authorization: Bearer dqk_…, and it acts with your org role and optional workspace scope. The raw token is shown once at mint time. See Create an API token for the full walkthrough and the personal-api-keys endpoints below to manage keys over the API.

Token lookups are cached in Redis for 5 seconds (TOKEN_CACHE_TTL_SECONDS). Revoked tokens stay accepted for at most that long.

Interactive API reference

This deployment serves its own OpenAPI schema and interactive docs, so the authoritative shape is always live alongside the running version:

PathWhat
/api/openapi.jsonThe raw OpenAPI 3 schema.
/api/docsSwagger UI — try endpoints in the browser.
/api/redocReDoc — a reference-style read-only rendering.

All three are reachable by authenticated users. In the hosted deployment the API sits behind IAP, so you reach them through the same login as the rest of the product. You can also dump the schema offline with dqc export-schema.

Response shape

Most endpoints wrap their payload:

{"data": ...}

Errors use FastAPI's default envelope:

{"detail": "human-readable error"}

with appropriate HTTP status codes (400 / 401 / 403 / 404 / 409 / 413 / 422 / 429 / 500).

File downloads are the documented exception. Export endpoints (e.g. GET .../validation-results/export) return a file attachment — Content-Disposition: attachment; filename="…" with a Content-Type of text/csv (default) or application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for ?format=xlsx — instead of the {"data": …} envelope.

Pagination

Where it's used, pagination is offset-based via ?limit=N&offset=M. Maximum limit per endpoint is 100 unless documented otherwise.

Rate limiting

Auth endpoints are rate-limited per IP and per IP+email (HARDEN-3, shipped). Mutation endpoints with abuse potential carry per-org token-bucket limits (HARDEN-18, shipped — see Configuration → Per-org rate limits for the bucket list). Rate-limited requests return 429 Too Many Requests with a Retry-After header.

Surface

The full list belongs in the OpenAPI schema. The high-level groups:

GroupPrefixWhat's in it
Auth/auth/...Login, refresh, logout. GET /auth/me/permissions returns the caller's role + flattened permission set (#169); GET /auth/me/principal returns the caller's identity — email, full_name, role, org_id, org_name, workspace_id — so the UI nav can show which account is active (#366). Both 404 / fail-closed for tokens with no associated user.
Accounts/organizations/{org_id}/accounts/meWho you are; org and workspace memberships.
Datasources/api/v2/organizations/{org_id}/workspaces/{ws_id}/datasources/...CRUD + test + discover. PATCH .../datasources/{id} rotates credentials in place — send config_encrypted.connection_string and it is re-encrypted at rest, shallow-merged over the stored config so sibling secrets survive; linked assets/suites/history are preserved. Requires can_edit_datasource + step-up auth (#313). POST .../datasources/{id}/discover/register promotes a set of discovered tables to first-class DataAsset rows in one transaction — body {"tables": [{"schema", "name", "kind"}, ...]}, returns {"data": {assets_registered, assets_skipped, asset_ids}}. Idempotent (existing names matched on datasource_id + name are skipped, not duplicated); gated on can_create_asset like GET .../discover (#456).
Data assets/api/v2/.../data-assets/...CRUD + profile-now.
Asset lineage/api/v1/.../data-assets/{id}/dependencies and /api/v1/.../data-assets/{id}/lineageList / create direct edges; walk the upstream + downstream graph up to ?depth=N hops (default 3, max 5). Manual edges only in v1 (FEATURE-24).
Batch definitions/api/v1/.../data-assets/{id}/batch-definitions and /api/v1/.../batch-definitions/{id}List / create batch slices per asset; get / delete by id. Three kinds: whole_asset, column_partition, interval. Editors read; owners and admins delete (FEATURE-36).
Rule suites/api/v1/rule-suites/...CRUD.
Asset-attached rules/api/v2/.../data-assets/{id}/rulesStatus: API shipped in #640 PR 4; the asset-page UI that surfaces it lands in PR 5. "The rules on a data asset" resolve to that asset's asset-scoped rule suite (the RuleSuite where data_asset_id == asset.id), one suite per asset. GET returns {"data": {data_asset_id, suite_id, rules}} — an empty rules list (not a 404) when the asset has no suite yet. PUT/POST create-or-update the asset's rule list (both replace it; the suite is lazily created the first time and validated via the shared rule-suite validator — a malformed custom-SQL rule → 422). DELETE clears the rule list (the suite row survives). POST .../rules/suggest kicks off an async AI rule suggestion for the asset, reusing the same generate_ai_suggestion pipeline as POST .../ai-suggestions (returns {"data": {suggestion_id, status}}; poll the suggestion via the AI endpoints). Reads gated can_view_suite (viewer+); writes can_edit_suite (editor+); suggest gated can_create_suite + the per-org AI rate limit, like the other LLM-spend endpoints.
Expectation gallery/api/v1/expectations/galleryRead-only catalogue of every form-builder expectation type — one-sentence description, example payload, kwargs schema, and applicable column types per entry. Tenant-independent (FEATURE-40).
Custom expectations/api/v1/organizations/{org_id}/custom-expectations and .../custom-expectations/packagesList the great_expectations.Expectation subclasses discovered from EXPECTATIONS_PLUGIN_PACKAGES; the /packages route surfaces per-package import status so an operator can spot import failures without tailing worker logs (FEATURE-35).
Checkpoints/api/v1/checkpoints/...CRUD + run-now. POST /api/v1/.../checkpoints/{id}/run-subset re-runs a checkpoint against an explicit list of batch_definition_ids for backfills (FEATURE-37).
Schedules/api/v1/checkpoints/{id}/schedules and /api/v1/schedules/{id}Cron schedules per checkpoint.
Team scopingteam_id on rule suites, validation configs, checkpoints, schedules, alert rulesOptional team pin on the five team-scoped resources. Send team_id in any create/update body: 422 "Invalid team for this workspace" if the team is unknown / deleted / in another org or workspace, 422 "Assigning a team requires a workspace-scoped resource" when there's no workspace anchor at all, 403 if you can't reach it (member or workspace-admin-or-higher). Omit it on create and the resource inherits the referenced parent's team (rule suite → checkpoint / validation config, checkpoint → schedule / alert rule); a body value that names a different team than the parent's is a 422, and an explicit null child under a team-scoped parent needs workspace-admin-or-higher (the child stays pinned to the parent's workspace). Un-scoping on update (team_id: null) also needs workspace-admin-or-higher and keeps the workspace pin. All five resources expose team_id in their response payloads. See Team-scoped resources.
Validation results/api/v1/validation-results/...List + detail. The list accepts optional filters: ?checkpoint_id=<uuid> (one checkpoint's runs — also powers the checkpoint detail page's run history), ?batch_definition_id=<uuid> to narrow to one partition, ?status=pass|fail, and an inclusive run_time window via ?start_date= / ?end_date= (ISO-8601). Filters compose (AND) and work alongside ?limit= / ?offset= pagination (#311 / #339 / #318 / #315). Results are minted by the worker after a checkpoint run and are read-only through the API: no write verb is registered, so POST/PUT/PATCH/DELETE return 405 Method Not Allowed — a result can't be forged, rewritten, re-linked, or deleted by a client.
Validation results — CSV / Excel exportGET /api/v1/organizations/{org_id}[/workspaces/{ws_id}]/validation-results/exportDownload the (filtered) results list as a file attachment (Content-Disposition: attachment). ?format= selects the container: csv (default, Content-Type: text/csv) or xlsx (Excel, Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet). Accepts the SAME filters as the list (?checkpoint_id= / ?status=pass|fail / ?start_date= / ?end_date= / ?batch_definition_id=), so a filtered export matches the on-screen filtered view. Both formats carry the same columns: run_time, checkpoint, data_asset, rule_suite, status, success, severity, batch_kind, partition_value, result_id — the CSV precedes them with a #-comment preamble (org/workspace id, export timestamp, filters); the xlsx puts the same metadata in a labelled block above a frozen header row. Capped at 50,000 rows — past that the endpoint returns 400 and asks you to narrow the date range. Gated by can_view_results (export what you can see). PDF export is the browser print dialog, not a server endpoint. See Export validation results (#408 / #664).
Validation results — reporting summaryGET /api/v1/organizations/{org_id}[/workspaces/{ws_id}]/validation-results/summaryThe read-only quality-report aggregate powering the Reports page. Returns {"data": {total_runs, passed, failed, pass_rate, trend, worst_checkpoints, filters}}pass_rate is a 0–1 fraction (or null when there are no runs in scope), trend is a per-day [{date, pass, fail}] series (oldest→newest), and worst_checkpoints is the top failing-checkpoint rollup [{checkpoint_id, checkpoint_name, failed, total}]. Accepts the SAME filters as the list / export (?checkpoint_id= / ?status=pass|fail / ?start_date= / ?end_date= / ?batch_definition_id=), so a filtered report's numbers match a filtered Results view (and a filtered CSV export). Gated by can_view_results. First cut of the reporting module — see Build a quality report (#407).
Results — "All workspaces" aggregatorGET /api/v1/organizations/{org_id}/results/allThe Super Admin org-wide roll-up (Phase-2 Unit 3). Returns {"data": [recent results], "summary": {total_count, by_workspace: {"<ws-uuid>": {count, passed, failed}, …}, by_status: {success, failed}}, "next_cursor"}. The summary is a SQL GROUP BY workspace_id over the same scope the data page walks, and is returned on the first page only — cursor pages return summary: null (the roll-up is identical across a walk; keep the first page's value). Results with workspace_id IS NULL (org-wide rows) appear under the "org_shared" key. Page rows omit the full result payload — fetch the result detail endpoint for it. data is cursor-paginated newest-first (?cursor= / ?limit=, default 50, max 200; next_cursor null on the last page) and both the page and the summary accept ?since=<ISO8601> (inclusive run_time lower bound; a naive timestamp is interpreted as UTC) and ?checkpoint_id=<uuid>. Requires an org-admin-tier role (super_admin; legacy owner/admin compat spellings) and can_view_results — callers without full-org visibility get a 403, not a partial roll-up. Agent (dqa_) / SCIM tokens are rejected.
Results — "All teams" aggregatorGET /api/v1/organizations/{org_id}/workspaces/{ws_id}/results/allThe Workspace Admin per-team roll-up (Phase-2 Unit 3). Returns {"data": [recent results], "summary": {total_count, by_team: {"<team-uuid>": {count, passed, failed}, …, "workspace_shared": {…}}}, "next_cursor"}. validation_results carries no team_id, so team ownership is derived via the parent checkpoint (GROUP BY checkpoints.team_id over a LEFT JOIN); results whose checkpoint is workspace-shared (team_id IS NULL) or that have no parent checkpoint at all (deleting a checkpoint detaches its results) land in the "workspace_shared" bucket. Gated by workspace membership + can_view_results; the roll-up never bypasses isolation — a workspace admin sees every team's bucket, a team-tier caller sees only their teams' + workspace_shared (a sibling team's results are absent from both data and the summary, and total_count always equals the rows the caller can page through). Same ?cursor=/?limit=/?since=/?checkpoint_id= params and first-page-only summary semantics as the org-level aggregator.
Dashboard metrics (workspace / team)GET /api/v1/organizations/{org_id}/workspaces/{ws_id}/dashboard/metrics?period=30The workspace-scoped Dashboard v2 roll-up — one read that powers every dashboard card (so each card reads a field instead of firing its own list query). ?period= is one of 30 | 60 | 90 | 365 days (the 30/60/90d/1y selector); anything else is a 422. Returns {"data": {period_days, health_score, pass_rate, total_runs, by_status: {passed, failed}, coverage: {covered, total, rate}, severity_counts: {critical, error, warning, info}, scope, team_id}}. health_score is 0–100 (or null when the period evaluated no expectations) computed with the same severity-weighted formula as the rule-group health score so the two agree; pass_rate is the whole-result success rate over the period as an integer % (or null with no runs); coverage is the share of in-scope data assets with ≥1 run in the window; severity_counts buckets the period's failed expectations by severity. ?team_id=<uuid> (#665) narrows the metrics to one team's slice — it must be a live team of this workspace (else 404), and health_score/pass_rate/total_runs/by_status/severity_counts/coverage.covered all narrow to that team's checkpoints+suites plus workspace-shared (team_id IS NULL) rows (a team-tier caller can never widen to a sibling team they can't see). coverage.total is the one metric that stays workspace-level under a team filter — data assets are workspace-shared by design and carry no team. scope echoes workspace / team; team_id echoes the resolved filter. Viewer-readable (can_view_results + workspace membership); isolation-respecting. Agent (dqa_) / SCIM tokens are rejected. (#657, #665)
Dashboard metrics (org roll-up)GET /api/v1/organizations/{org_id}/dashboard/metrics?period=30The org-wide Dashboard v2 roll-up (#665): the same metrics aggregated across the caller's accessible workspaces (no {workspace_id} in the path, so the workspace clamp never fires). Same response shape as the workspace endpoint, with scope: "org" and team_id: null. Org-admin-tier ONLY — mirrors the results/all org aggregator exactly: a caller below super_admin (or the legacy owner/admin compat spellings) gets a 403, not a partial roll-up, because an org-wide roll-up only makes sense for a caller with full-org visibility. The metric SQL is shared with the workspace tier so the two can't drift. No ?team_id= (team is a within-workspace concept). Agent (dqa_) / SCIM tokens are rejected. (#665)
Result comments/api/v1/.../validation-results/{id}/comments/...Annotations on validation results: list, create, edit (author/owner), delete (author/owner).
Root cause analysisGET /api/v1/.../validation-results/{id}/rcaRead-only correlation of a failed result's signals into ranked likely causes. Viewer-readable. Optional AI narrative via ?include_narrative=true. Detailed below (FEATURE-410).
Profile runs/api/v1/ai/profile-runs/...List + detail + run-now.
Anomalies/api/v2/.../anomaly-events/...List + ack.
AI suggestions/api/v2/.../ai-suggestions/... and /api/v1/ai/...List + accept + dismiss + explain.
AI bulk actionsPOST /api/v1/organizations/{org_id}/workspaces/{ws_id}/ai-suggestions/bulk-accept and .../bulk-dismissAccept or dismiss many AI suggestions in one round trip; step-up auth + admin/owner role required. Shipped in FEATURE-39.
Alert rules/api/v1/alert-rules/... and .../alert-rules/{id}/eventsCRUD + test-send. The /events sub-collection is the per-rule dispatch / fire log (#655) — GET lists the rule's AlertEvent rows (status pending / sent / failed, sent_at, channel, error) newest-first with opt-in cursor pagination, gated on can_view_alert (viewer+). Any secret that leaked into an error (a webhook URL's secret path / a token) is redacted out of the response.
Incidents/api/v1/organizations/{org_id}[/workspaces/{ws_id}]/incidents[/{id}] and .../incidents/{id}/eventsNative incident lifecycle (#411). List / get gated on can_view_incident (viewer+); create / PATCH (status open → acknowledged → resolved + assignee/notes edits) / delete + timeline POST gated on can_manage_incident (editor+). The list supports opt-in cursor pagination. A PATCH that flips status stamps acknowledged_at / resolved_at and appends a status_change timeline entry carrying {from, to}. The /events sub-collection is the append-only timeline — GET lists it, POST {data:{body}} adds a comment whose author is the authenticated principal (never the body). A failed checkpoint run auto-opens one incident (source=auto) idempotently per checkpoint. See Incidents + Manage incidents.
Agent tokens/api/v1/agent-tokens/...CRUD + revoke (for on-prem agents).
Personal API keys/api/v1/organizations/{org_id}/personal-api-keys/...List / mint / revoke your own dqk_ keys. You only ever see and act on your own keys. Mint is step-up gated + rate-limited (rate_limit_personal_api_keys_per_minute). Detailed below.
Invites/api/v1/organizations/{org_id}/invites/...Mint, list pending, revoke. Accept via POST /auth/accept-invite.
Workspace members/api/v1/organizations/{org_id}/workspaces/{ws_id}/members/...List, change role, remove.
Org-wide membersGET /api/v1/organizations/{org_id}/membersThe Super Admin org-wide member roster (#649). Returns {"data": [{user_id, email, full_name, org_role, workspaces: [{workspace_id, name, role}, …], teams: [{team_id, name, role, workspace_id}, …]}, …]} — one row per org member, listing every workspace + team they belong to across the whole org regardless of the selected workspace, each with its per-membership role. Requires an org-admin-tier role (super_admin; legacy owner/admin compat spellings) — a workspace_admin / technical_admin / team role gets a 403 and keeps the workspace-scoped …/workspaces/{ws_id}/members view instead. Read-only; per-membership edits stay on the workspace-members + Teams endpoints.
LLM configs/api/v2/organizations/{org_id}/llm-configs/...CRUD.
Identity providers/api/v1/organizations/{org_id}/identity-providers/...CRUD for SAML / OIDC providers (FEATURE-31). Owner-only. Login flow is GET /auth/sso/{org_slug}/start + GET /auth/sso/{org_slug}/callback. See Configure SSO.
Audit events/api/v1/organizations/{org_id}/audit-eventsRead-only audit log (HARDEN-21) — covers logins (success + failure), logout, tenant provisioning, agent-token mint/revoke, LLM-config + datasource + alert-rule + rule-suite + data-asset + incident writes and deletes, AI suggestion bulk actions. Owner + admin only (widened from owner-only in #196 so the per-resource History button works for admins). Pass ?resource_type=<kind>&resource_id=<uuid> together to scope the list to one resource's history (FEATURE-8); the UI surfaces the slice via a "History" button on each detail page. The underlying audit_events / global_audit_events tables are append-only at the DB layer (a trigger blocks UPDATE/DELETE; the retention prune is the only sanctioned delete).
Step-up authPOST /auth/elevateRefresh auth_fresh_at with a fresh password proof (HARDEN-24). Required before mutating bearer-secret-bearing resources — agent tokens, LLM configs, datasources, alert rules. The UI catches 401 detail=step_up_required and prompts inline.
Data docs/api/v1/organizations/{org_id}/data-docs-config and .../data-docs-config/rebuildGet/set the static data-docs destination URI; trigger an out-of-cycle build. Owner-only. See publish a data-docs site.
Branding/api/v1/organizations/{org_id}/branding and .../branding/logoThe org-configurable sidebar logo (#647). GET .../branding returns {"data": {logo_light_type, logo_dark_type, has_light, has_dark, logo_light, logo_dark, updated_at}} where each logo_* is an inline data: URI carrying its own per-variant type (or null when unset) — readable by any org member (the logo shows in everyone's sidebar). GET .../branding/logo?variant=light|dark streams the raw bytes with that variant's own content type; SVG variants are served nosniff + a sandboxed CSP so a directly-opened mark can't execute embedded script. PUT .../branding/logo sets/replaces the logo (body {content_type, logo_light?, logo_dark?, logo_light_type?, logo_dark_type?}, the logo_* fields base64-encoded); content_type is the default type, with optional per-variant logo_*_type overrides so the two marks may differ in format (e.g. SVG light + PNG dark). At least one variant is required, each variant ≤ 256 KB, every type ∈ {image/png, image/jpeg, image/svg+xml}. DELETE .../branding/logo resets to the default MeasuredCloud wordmark. Mutations are org-admin only (can_manage_org — super admin / owner) + step-up gated; an unset logo simply falls back to the wordmark (no default is stored). See Customize your organization's logo.
Catalog sync/api/v1/organizations/{org_id}/catalog-sync-config and .../catalog-sync/testGet/set the outbound catalog-sync webhook URL + optional HMAC signing secret; fire a synthetic test event. Owner-only. POSTs a stable JSON envelope after every validation result. See Integrate with your data catalog.
Catalog integrations/api/v1/organizations/{org_id}/catalog-integrations[/{id}[/sync]]CRUD + "Sync now" for first-party (native) catalog connectors — one row per (org, kind). Accepted kind values: datahub, atlan, data.world, alation (#403 / #404 / #415). Admin + owner (can_manage_integrations); writes are step-up-gated + audit-logged. The auth_token is AES-256-GCM encrypted at rest and redacted to the "configured" sentinel on read. DataHub also does inbound discovery; the others are outbound-only. See DataHub, Atlan, Data.world, Alation.
dbt ingestPOST /api/v1/organizations/{org_id}/workspaces/{ws_id}/dbt/ingestSynchronously parse a dbt manifest.json + run_results.json pair: upsert one DataAsset per model node, write one synthetic ValidationResult per matched run-result. Re-ingest is idempotent. See Ingest dbt artifacts.
SearchGET /api/v1/organizations/{org_id}/workspaces/{ws_id}/search?q=<term>Unified, workspace-scoped global search across datasources, data assets, rule suites, rule groups, checkpoints, schedules, alerts, incidents, validation results, and column names (matched inside the latest profile's column_stats). Returns a flat grouped list of hits — each carries type, id, label, optional description, route, and group. Optional ?type=<kind> narrows to one hit type; ?limit= (1–25, default 8) caps hits per type. Any workspace member may search; a blank q returns {"data": []} (no full-table dump). No secret/DSN/ciphertext ever crosses the boundary. Powers the always-visible top-bar search box and the Cmd+K palette — see Search across objects (#455, #667).
JWKS/.well-known/jwks.jsonPublic keys for verifying RS256 JWTs. Gated on JWT_PUBLISH_JWKS=true (HARDEN-13).
DeploymentGET /api/v1/deploymentPublic, unauthenticated metadata about this deployment: region, version, sso_enabled, llm_provider. Lets a customer / auditor confirm where their data is hosted without signing in. See FEATURE-34.
Health/healthLiveness probe.

Rate limiting (per-org)

Mutation endpoints with abuse potential carry per-org token-bucket limits (HARDEN-18). Over-limit hits return 429 Too Many Requests with Retry-After. See Configuration → Per-org rate limits for the list and defaults.

Personal API keys

Self-service management of your own dqk_ keys. Every route is scoped to the org in the path; you only ever see and act on keys you minted. A minted key inherits your org role and, if workspace_id is set, your access to that one workspace. See Create an API token for the UI walkthrough.

GET /api/v1/organizations/{org_id}/personal-api-keys

List your keys in this org. The raw token is never returned — only metadata.

{
"data": [
{
"id": "key-uuid",
"name": "ci-nightly",
"workspace_id": null,
"last_used_at": "2026-05-28T11:04:00Z",
"created_at": "2026-05-01T09:00:00Z"
}
]
}

POST /api/v1/organizations/{org_id}/personal-api-keys

Mint a new key. Step-up gated (a recent password proof — see Step-up auth / POST /auth/elevate) and rate-limited by rate_limit_personal_api_keys_per_minute (default 10; over-limit → 429 with Retry-After).

Request:

{"data": {"name": "ci-nightly", "workspace_id": null}}

workspace_id is optional — null (or omitted) makes the key mirror your own access: for org owners/admins that's the whole org, for everyone else it's exactly the workspaces you can reach through your memberships (direct or via a team) — an unpinned key never reads a workspace your own login couldn't. A workspace UUID scopes the key to that one workspace.

Breaking change (Phase-2 defense-in-depth). Unpinned (workspace_id: null) keys held by non-admin users used to inherit full-org access; they are now clamped to the holder's own membership-derived workspace set. An existing unpinned key that was reading a workspace its holder isn't a member of flips from 200 to 404 on those requests. Remedies: add the key's holder to the workspace (directly or via a team), have an org owner/admin re-mint the key under their own identity, or mint a key pinned to the specific workspace. Org owner/admin keys are unaffected.

Response — the only time the raw token is ever returned:

{
"data": {
"id": "key-uuid",
"name": "ci-nightly",
"workspace_id": null,
"last_used_at": null,
"created_at": "2026-05-29T12:00:00Z",
"token": "dqk_..."
}
}

Store the token immediately — only its hash is kept, so it can't be shown again.

DELETE /api/v1/organizations/{org_id}/personal-api-keys/{id}

Revoke one of your keys. Returns 204 No Content. Revocation isn't instant: a revoked key keeps working for up to TOKEN_CACHE_TTL_SECONDS (default 5) until its cached lookup expires.

Root cause analysis

Status: first cut (FEATURE-410). Heuristic correlation + optional AI narrative; ML causal inference and several signal families are deferred. See Root cause analysis.

GET /api/v1/organizations/{org_id}/workspaces/{workspace_id}/validation-results/{result_id}/rca

Correlate a failed validation result's already-available signals — failing expectations, cross-run failure pattern, last-passing-vs-current statistical diff, recent anomalies, and schema drift — into a ranked likely-cause summary. Read-only; nothing is persisted.

Readable by any role with can_view_results (viewer included) — it is read-augmentation of a result you can already see. A cross-org org_id is rejected with 403; an unknown result_id returns 404.

Query parameters

ParamDefaultEffect
include_narrativefalseWhen true and a default LLM config is set, appends a plain-English AI narrative. Off by default to bound LLM spend.

Response{"data": RcaResponse}:

FieldNotes
applicablefalse for a passing result (RCA is for failures) — everything else is then empty/default.
data_asset_id / data_asset_resolvedThe linked asset, when resolvable from the checkpoint / validation config. data_asset_resolved: false → expectation-only RCA (no stat diffs / anomalies).
failure_patternnew | recurring | regression | unknown.
last_passed_at / first_failed_atThe RCA timeline. Either may be null.
failing_expectations[]{expectation_type, column, description, unexpected_count, observed_value, severity}.
stat_diffs[]{metric, column, previous, current, delta, pct_change}row_count plus per-failing-column null_rate/min/max/mean.
contributing_factors[]Ranked likely causes: {tag, title, detail, confidence, evidence}, most-confident first.
narrative / narrative_statusThe optional AI text and its state: generated | no_llm_config | not_requested | error.
llm_config_id / prompt_tokens / completion_tokensPresent only when a narrative was generated.

The narrative path is covered by the per-org ai_suggestions rate limit, and no credential, ciphertext, or DSN ever appears in the response.

Webhooks

The generic-webhook alert channel (FEATURE-16) POSTs a JSON envelope to your operator-supplied URL. The canonical payload shape, signing scheme, and reference verification snippet live in Alerts → Generic webhook.

SDKs

Hand-rolling requests/fetch works fine, but the typed clients are usually quicker: