REST API¶
DataQ's REST API is the same surface the web UI uses — versioned under /api/v1, with the
same per-suite authorization. Use it for scripting, CI integration, or your own tooling.
Base URL & auth¶
The API has no public ingress of its own; reach it through the frontend host, which
proxies /api same-origin (ADR 0028 §5):
https://<your-frontend-host>/api/v1/...
Authenticate with a personal access token (mint one in the UI → Profile → API keys, see API keys) as a Bearer token:
BASE=https://<your-frontend-host>/api/v1
TOKEN=dq_live_xxxxxxxx # keep it out of shell history / source
curl -s -H "Authorization: Bearer $TOKEN" $BASE/me
A PAT acts as its owning user — every call is scoped by the same two axes as the UI: your
workspace role (admin | member | viewer, ADR 0033) and your per-suite view/edit
grants. Both resolve per request, so a role change or a revoked share applies to keys you
already hold, on their very next call. Unauthenticated requests get 401; an authenticated
call your role doesn't permit gets 403. (The interactive Swagger/OpenAPI docs are disabled
in production — this page is the reference.)
Conventions¶
- Versioning: all endpoints are under
/api/v1. - Errors: a JSON envelope —
{"error": {"code": "...", "message": "...", "detail": {...}}}— with a conventional HTTP status (401auth,403forbidden,404not found / hidden,422validation,409conflict,429rate limited,502datasource unreachable). Named codes worth knowing:rate_limited(see below), andcredential_redirectonPATCH /connections/{id}— see Connections. - Rate limiting (ADR 0035): every surface is throttled per minute — authenticated
requests per API key, unauthenticated per client-IP prefix (IPv4 /24, IPv6 /64 by default — machines on one allocation share a budget). Over the limit returns
429withcode: "rate_limited",detail.retry_after_seconds(1–60), and a matchingRetry-Afterheader (plusX-RateLimit-Limit/X-RateLimit-Remaining). Back off for that many seconds. - IDs are UUIDs. Timestamps are ISO-8601 UTC.
Endpoints¶
Identity & sign-in¶
| Method | Path | What |
|---|---|---|
| GET / PATCH | /me |
The current user + role (admin / member / viewer) + is_workspace_admin; PATCH updates display_name. |
| POST | /me/api-keys |
Mint a PAT (plaintext returned once). |
| GET | /me/api-keys |
List your keys (metadata only, never the token). |
| DELETE | /me/api-keys/{id} |
Revoke a key. |
| POST | /auth/otp/request · /auth/otp/verify |
Email-OTP sign-in (ADR 0032): request a code, redeem it for a dq_sess_ cookie session. Uniform responses — never confirms whether an address is enrolled. |
| POST | /auth/logout |
Revoke the OTP session server-side. |
| GET | /users/search |
Find a user by name/email (for sharing). Viewer results are edit-clamped. |
Connections¶
| Method | Path | What |
|---|---|---|
| GET / POST | /connections |
List / create a connection. |
| GET / PATCH / DELETE | /connections/{id} |
Read / update / delete. |
| POST | /connections/test |
Test an unsaved draft connection — nothing is persisted. |
| POST | /connections/{id}/test |
Test live connectivity. |
| POST | /connections/{id}/reauth |
Rotate the credential and verify. |
| GET | /connections/{id}/versions |
Config-change history (ADR 0020 snapshots — never credentials). |
Roles (ADR 0033). Connections are shared infrastructure holding credentials, so the gates
here are the sharpest in the API: create, update, delete, re-auth and the unsaved-draft
POST /connections/test are Admin-only; the saved-connection POST /connections/{id}/test
is Member+; list and read are open to any authenticated user (responses carry has_secret,
never secret material). A Member's PAT hitting POST /connections gets 403 regardless of
any suite grant — the two axes are independent.
Credential health. Each datasource connection in GET /connections carries a
credential_health object — status (healthy / failing / unknown),
consecutive_auth_failures, last_auth_success_at, last_auth_failure_at and a classified
last_error. It is derived from real use (runs, dry-runs, profiles and
POST /connections/{id}/test), never a periodic probe, and only a credential rejection
moves it: a missing grant, an unreachable host and a bad table name leave it untouched.
unknown means the credential has not been used yet and is never reported as healthy.
Orchestration connections carry null here — theirs is the poll health above.
GET /admin/health returns the same shape for every datasource connection at once, worst
status first.
Moving a connection to a new host. A PATCH that changes a field deciding where the
credential is sent — account (Snowflake), account_url (ADLS), endpoint_url (S3, dbt),
workspace_url (Unity Catalog), catalog_uri / warehouse / properties /
secret_property (Iceberg),
base_url (Airflow), artifacts_uri (dbt) — must re-supply that credential in the same
request. Otherwise it returns 422 with code: "credential_redirect" and
detail.required naming what to send. A stored credential is never forwarded to a
destination the caller changed.
Notification channels¶
A reusable Teams/Slack/email/generic-webhook destination, defined once and referenced from any
number of suites — the destination only; per-suite alert_on/enabled stays on
/suites/{id}/notifications above.
A webhook channel posts an HMAC-SHA256-signed JSON body (header X-DataQ-Signature) to an
admin-supplied webhook_url — the vendor-neutral way to reach PagerDuty, Opsgenie, ServiceNow,
Jira, or a self-hosted receiver with no per-vendor code. The destination URL must be https and
must not resolve to a private, loopback, or otherwise internal address (an SSRF guard, checked
both when the channel is saved and again before every send). DataQ generates the signing key —
it is returned in the response body exactly once, at creation (or at rotation via
regenerate_hmac_secret: true on PATCH), and is never retrievable again after that.
A webhook channel may also carry an optional payload_template — a JSON object reshaping the
generic alert body for a specific receiver (a PagerDuty Events-API shape, an Opsgenie/ServiceNow/
Jira payload) — and an optional auth_header_name + auth_header_value pair, an extra header
some receivers want beside the signature. A template's {{field.path}} placeholders resolve by
key lookup only against the same fields the generic payload already carries; there is no
expression language, so a template can rename or select what's there but never reach a field
that isn't. auth_header_value is write-only, same as every other channel credential. Leaving
both unset keeps the channel sending the plain generic payload with no extra header — the
pre-template behavior, unchanged.
payload_template is stored as plain JSON, not a SecretStore-backed credential — but a template
commonly has nowhere else to put a receiver's static routing/integration key than as a literal
value in the JSON, so GET/LIST only ever include it for a workspace Admin caller; every
other authenticated user gets has_payload_template (a presence-only boolean) instead, the same
shape already used for genuine secrets. Put any real credential in auth_header_value instead —
it's encrypted at rest and never echoed back to anyone. Changing webhook_url on a channel that
already has a stored auth header requires re-supplying auth_header_value in the same request
(422 channel_credential_redirect otherwise) — silently repointing the destination must never
carry a stored credential to a URL the caller didn't just prove they still control.
| Method | Path | What |
|---|---|---|
| GET / POST | /notification-channels |
List / create a channel. |
| GET / PATCH / DELETE | /notification-channels/{id} |
Read / update / delete (refused with 409 channel_in_use while any suite still references it). |
| GET | /suites/{id}/notification-channels |
List a suite's linked channels. |
| PUT / DELETE | /suites/{id}/notification-channels/{channel_id} |
Link / unlink a channel (linking is idempotent — relinking is a no-op, not a conflict). |
Roles. Same split as connections: create/update/delete are Admin-only (a webhook URL is
a credential); list/read are open to any authenticated user (has_webhook only, never the
URL); linking/unlinking a suite follows that suite's own view/edit grant.
Suites & checks¶
| Method | Path | What |
|---|---|---|
| GET / POST | /suites |
List / create a suite. |
| GET / PATCH / DELETE | /suites/{id} |
Read / update / delete. |
| GET / POST | /suites/{id}/checks |
List / add checks. |
| GET / PATCH / DELETE | /suites/{id}/checks/{cid} |
Read / update / delete a check. |
| POST | /suites/{id}/checks/dryrun |
Preview a check against live data (no persistence). |
| POST | /suites/{id}/checks/{cid}/snooze · DELETE to clear |
Snooze a check's alerts for N hours. |
| GET | /suites/{id}/checks/{cid}/versions · POST …/versions/{n}/restore |
Version history + restore (restore mints a new version). |
| GET | /suites/{id}/checks/{cid}/history |
Result history for the trend view (metric_value over time). |
| GET | /suites/{id}/checks/{cid}/baseline · POST …/rebaseline |
Read / recapture a monitor baseline (schema-drift, anomaly). |
| GET | /suites/{id}/export · POST /suites/import |
Portable suite document (env promotion). |
| GET / PUT | /suites/{id}/column-policy |
Read / set the failing-sample redaction policy. |
| POST | /suites/{id}/column-policy/suggest |
Heuristic PII-column suggestions from a profile. |
| POST | /suites/{id}/profile |
Column profiler (no persistence; audited as a data access). |
| GET | /suites/{id}/columns |
Column names/types of the resolved target (cheap authoring aid). |
| GET | /suites/{id}/batch-preview |
Which files a flat-file batch pattern currently matches. |
| GET / POST | /suites/{id}/shares |
List / grant per-suite access (view / edit). |
| PATCH / DELETE | /suites/{id}/shares/{user_id} |
Change / revoke a grant (Viewers cap at view). |
| GET / PUT / DELETE | /suites/{id}/notifications |
Per-suite alert config (channels, alert_on, auto-resolve). |
Running & results¶
| Method | Path | What |
|---|---|---|
| POST | /suites/{id}/run |
Trigger a run (returns a run id to poll). |
| GET | /runs · /runs/{id} |
List runs / get a run with its results (reads are access-audited — ADR 0041). |
| GET | /runs/{id}/progress |
Live per-check progress. |
| POST | /runs/{id}/cancel |
Cancel a queued/running run. |
| GET | /runs/{id}/results/{rid}/comparison_report |
CSV/XLSX diff report of a comparison result (derived on demand, never stored). |
| GET | /dashboard/summary |
KPIs + run trend + per-suite performance. |
Assets & incidents¶
| Method | Path | What |
|---|---|---|
| GET | /assets · /assets/{id} |
The monitored tables/files (ADR 0034/0037): health rollup, composing suites (grant-filtered), lineage. Paged, X-Total-Count. |
| PATCH | /assets/{id} |
Set owner / description (workspace-Admin-only). |
| GET | /incidents · /incidents/{id} |
Open/acknowledged/resolved incidents with the evidence card (suite-granted; 404-no-leak). |
| POST | /incidents/{id}/ack · /incidents/{id}/resolve |
Lifecycle transitions (requires edit on the suite). |
Scheduling & orchestration¶
| Method | Path | What |
|---|---|---|
| GET / POST | /schedules |
List / create cron schedules. |
| GET / PATCH / DELETE | /schedules/{id} |
Read / update / delete. |
| GET / POST | /trigger-bindings |
List / create pipeline→suite trigger bindings. |
| GET / PATCH / DELETE | /trigger-bindings/{id} |
Read / update (incl. enable/disable) / delete. |
| GET | /pipeline_runs · /orchestration/pipelines |
Monitored orchestrator runs. |
| GET | /orchestration/near-misses |
Succeeded pipeline runs that matched no enabled binding (why a trigger never fired). |
| POST | /orchestration/events/{provider} |
Inbound webhook (adf / airflow / dbt) — authenticated by shared-secret / HMAC, not a PAT. |
LLM-assisted authoring (ADR 0042)¶
Each POST queues an async invocation (202, LlmInvocationQueued) run by the worker
against the configured LLMProvider; poll GET /llm/invocations/{id} for the result.
Rate-limited under a dedicated llm class (per-principal and per-IP), since each call is
an outbound model request.
| Method | Path | What |
|---|---|---|
| POST | /llm/sql_generation |
Draft a custom-SQL check from a natural-language rule (suite edit). |
| POST | /llm/check_suggestions |
Suggest checks for a suite from its column profile (suite edit). |
| POST | /llm/rca_narrative |
Root-cause narrative for a failed check, from an incident's evidence card. |
| GET | /llm/invocations/{id} |
Poll one invocation — requester or workspace admin only. |
Admin (workspace-admin only)¶
| Method | Path | What |
|---|---|---|
| GET | /admin/suites · /admin/users · /admin/access |
Unscoped workspace-wide views. |
| GET | /admin/overview |
The four Overview stat cards — members, suites, open incidents, runs today (UTC day). |
| PATCH | /admin/users/{id}/role |
Change a workspace role (last-admin guarded; demoting yourself needs confirm_self; audit-tabled). |
| DELETE | /admin/suites/{id}/access/{grant_id} |
Revoke any per-suite share, on any suite (audited as an admin override). |
| POST | /admin/suites/{id}/transfer |
Transfer suite ownership. A viewer cannot own (422); transferring to the current owner is a 409. The previous owner keeps an edit grant unless keep_previous_owner_access is false. |
| DELETE | /admin/suites/{id} |
Delete any suite. Same cascade as the owner's own delete; the audit event carries the destroyed counts. |
| GET | /admin/members |
Workspace membership, with whether enforcement is on and how many users a first add would import. |
| POST | /admin/members |
Admit an address, with an optional initial role. The first add turns enforcement on and imports existing users for review (audited). |
| DELETE | /admin/members/{id} |
Withdraw a membership — bites on the next request for every credential kind. Last-admin guarded; confirm_self=true to remove your own (audited). |
| POST | /admin/members/{id}/confirm |
Clear the provisional flag on an imported member. Grants nothing new (audited). |
| GET | /admin/offboarding/{user_id}/preview |
What offboarding this user would do: suites they own with counts, live tokens and sessions, whether membership can be withdrawn here, and whether they are the last admin. Reserves nothing. |
| POST | /admin/offboarding/{user_id} |
Transfer every suite they own, revoke every token and session, withdraw their membership — one transaction. Last-admin guarded; confirm_email must match. A step that cannot run is reported in skipped with its reason. Authored history is kept (audited). |
| GET | /admin/audit-events |
The append-only audit log (config + data-access events, ADR 0041). |
| GET | /admin/deployment |
Declared residency / deployment posture (DEPLOYMENT_REGION); zero_sample_mode is the effective value and zero_sample_source says which of env / setting turned it on. |
| GET | /admin/privacy |
Zero-sample mode: effective value, stored toggle, source, whether the environment pins it. |
| PUT | /admin/privacy |
Set zero-sample mode (audited); 409 when the environment pins it on and the request turns it off. |
| GET | /admin/scoring |
Health-score penalty weights in force, whether they are the defaults, and the defaults themselves. |
| PUT | /admin/scoring |
Set the weights (audited); 422 unless 0 ≤ warn ≤ fail ≤ critical and critical > 0. |
| DELETE | /admin/scoring |
Reset the weights to the defaults (audited). |
| GET | /admin/orchestration/webhooks |
Webhook receiver URLs + auth mode per provider. |
| POST | /admin/orchestration/webhooks/{provider}/regenerate |
Mint a new webhook secret / signing key; the value is returned once, the previous one is accepted until grace_until (audited). |
| POST | /admin/orchestration/poll-now |
Queue an immediate orchestration poll for every connection, or one with ?connection_id=. |
| GET | /admin/inventory-sync |
Per-connection inventory-sync state; counts are null until a sync has run. |
| PATCH | /admin/inventory-sync/{connection_id} |
Turn inventory sync on or off (through the connection-update path: versioned, audited). |
| POST | /admin/inventory-sync/{connection_id}/run |
Queue one inventory sync now. |
| GET | /admin/secret-sweep |
Last orphan-secret sweep report — never_run if the sweep hasn't recorded one. |
| POST | /admin/secret-sweep/run |
Run the orphan-secret sweep now, always report-only (audited). |