Skip to content

Querycop API Reference Community

Spec: docs/openapi.yaml is the source of truth for the route surface (OpenAPI 3.1). CI runs go run ./tools/check_openapi_routes which AST-parses pkg/api/handler.go and diffs the registered routes against the spec; any drift fails the build. Run make check-openapi locally to verify before opening a PR.

This Markdown reference exists for human-readable narrative — request/response examples, security notes, RBAC behavior. When in doubt about which routes exist, the YAML is authoritative.

Base URL: http://localhost:8080

Authentication: Bearer Token via Authorization: Bearer <ADMIN_API_KEY> header. Endpoints marked with (locked) require authentication when ADMIN_API_KEY is set.

Rate Limit: 10 requests/second per IP (HTTP 429 when exceeded). Health-check endpoints (/healthz, /readyz) are exempt from rate limiting.

Unauthenticated Endpoints: /healthz, /readyz, /metrics, /ws, / (dashboard)

Max Request Body Size: 1 MB for all endpoints accepting a body (HTTP 413 when exceeded).


Liveness probe. Always returns 200 if the process is running.

  • Auth: Not required
  • Response: 200 OK
{"status": "ok"}

Readiness probe. Returns 200 if backend DB is reachable via TCP.

  • Auth: Not required
  • Response: 200 OK or 503 Service Unavailable

Success:

{"status": "ready"}

Failure:

{"status": "not ready", "reason": "backend unreachable"}

Prometheus-compatible metrics in text exposition format (0.0.4).

  • Auth: Not required
  • Content-Type: text/plain; version=0.0.4; charset=utf-8
  • Response: 200 OK — Prometheus text format body
  • Availability: Only registered when a metrics collector is configured.

Dashboard UI (single-page HTML application). Serves the embedded index.html. Any path that does not match a registered route or static file also falls back to index.html (SPA routing).

  • Auth: Not required
  • Content-Type: text/html; charset=utf-8
  • Response: 200 OK

List pending approval requests.

  • Response: 200 OK
[
{
"id": "a1b2c3...",
"query": "DELETE FROM users WHERE id=1",
"db_user": "testuser",
"created_at": "2026-03-27T12:00:00Z",
"risk_score": 85,
"risk_reason": "WHERE clause missing"
}
]

Approve a pending query.

  • Query Params: id (required) — the request ID
  • Response: 200 OK
{"status": "approved"}
  • Errors:
    • 400 Bad Request — missing id parameter
    • 404 Not Found — request not found

Reject a pending query and disconnect the client.

  • Query Params: id (required) — the request ID
  • Response: 200 OK
{"status": "rejected"}
  • Errors:
    • 400 Bad Request — missing id parameter
    • 404 Not Found — request not found

Search audit log entries with optional filters.

  • Source: This endpoint reads from the in-memory audit store (max 10,000 entries, lost on restart). The optional JSONL file logger (GATEKEEPER_AUDIT_FILE) and the optional HMAC tamper-evidence chain (GATEKEEPER_AUDIT_HMAC_KEY) are not exposed here — entries returned by this endpoint will not contain prev_hash / hash fields. To verify the tamper-evidence chain, read the JSONL file directly (see docs/configuration.md § 10) or use pkg/audit.VerifyChain.
  • Tier behavior: the in-memory window is Core — available on every tier, including Community. MultiLogger.Query is wired so that when persistent_audit is license-disabled the query falls back to the in-memory logger; the dashboard’s Audit Log tab works on Community without hitting any 403. Persistent (file/JSONL + HMAC chain) audit is the Pro persistent_audit feature.
  • Query Params:
    • since — start time (RFC 3339 or YYYY-MM-DD)
    • until — end time (RFC 3339 or YYYY-MM-DD)
    • user — DB username filter
    • type — event type filter. One of: passthrough, blocked, approved, rejected, timeout, policy_deny, policy_allow, auto_approved, break_glass, data_guard_violation
    • limit — max entries to return (integer, default 100)
  • Response: 200 OK — JSON array of audit entries
  • Errors:
    • 400 Bad Request — invalid since, until, or limit parameter
    • 500 Internal Server Error — query failed
    • 503 Service Unavailable — audit log not enabled

Get current policy configuration as JSON.

  • Availability: Only registered when a policy engine is configured.
  • Response: 200 OK
{
"roles": [...],
"rules": [...]
}

Update policy configuration (hot reload). Replaces the entire policy with the submitted JSON.

  • Availability: Only registered when a policy engine is configured.
  • Body: JSON policy object
  • Max Body Size: 1 MB
  • Response: 200 OK
{"status": "policy updated"}
  • Errors:
    • 413 Request Entity Too Large — body exceeds 1 MB
    • 400 Bad Request — failed to read request body
    • 500 Internal Server Error — policy load failed

Review a SQL migration batch for risk assessment. Parses the input into individual statements and scores each one.

  • Availability: Always registered (E4-A3: registered unconditionally so the license gate and config gate are both reachable). Behavior:
    • License does not include cicd_gate403 Forbidden with the canonical envelope (see “License-gated routes” below).
    • License is OK but no reviewer is wired (WithReviewer not passed) → 503 Service Unavailable with {"error":"review service not configured"}.
    • License + reviewer OK → normal 200 OK with the batch review result.
  • Body:
{"sql": "ALTER TABLE users ADD COLUMN age INT; DROP INDEX idx_name;"}
  • Response: 200 OK
{
"results": [
{
"query": "ALTER TABLE users ADD COLUMN age INT",
"risk_score": 45,
"risk_level": "medium",
"reasons": ["ALTER TABLE"]
}
],
"max_score": 72,
"overall_risk": "high",
"total_queries": 2
}
  • Errors:
    • 413 Request Entity Too Large — body exceeds 1 MB
    • 400 Bad Request — invalid request body or missing sql field
    • 500 Internal Server Error — review failed

Get all configured masking rules.

  • Availability: Only registered when a masking engine is configured.
  • Response: 200 OK
[
{
"column": "email",
"mask_type": "partial",
"table": "users"
}
]

Add a new masking rule.

  • Availability: Only registered when a masking engine is configured.
  • Body:
{
"column": "email",
"mask_type": "partial",
"table": "users"
}
  • Required fields: column, mask_type
  • Response: 201 Created
{"status": "rule added"}
  • Errors:
    • 413 Request Entity Too Large — body exceeds 1 MB
    • 400 Bad Request — invalid body, missing column, or missing mask_type

Replace the entire masking configuration.

  • Availability: Only registered when a masking engine is configured.
  • Body: JSON masking config object
  • Response: 200 OK
{"status": "masking config updated"}
  • Errors:
    • 413 Request Entity Too Large — body exceeds 1 MB
    • 400 Bad Request — invalid request body

Activate break-glass mode, bypassing normal approval flow for emergency access.

  • Availability: Only registered when a break-glass manager is configured.
  • Body:
{
"requester": "oncall-alice",
"reason": "prod incident INC-1234",
"ttl_seconds": 600
}
  • Response: 201 Created — JSON session object
{
"id": "...",
"requester": "oncall-alice",
"reason": "prod incident INC-1234",
"activated_at": "2026-03-27T12:00:00Z",
"expires_at": "2026-03-27T12:10:00Z",
"active": true
}
  • Errors:
    • 413 Request Entity Too Large — body exceeds 1 MB
    • 400 Bad Request — invalid body or activation failed

Deactivate break-glass mode.

  • Availability: Only registered when a break-glass manager is configured.
  • Response: 200 OK
{"status": "deactivated"}
  • Errors:
    • 500 Internal Server Error — deactivation failed

Get current break-glass session status.

  • Availability: Only registered when a break-glass manager is configured.
  • Response: 200 OK

When inactive:

{"active": false}

When active:

{
"id": "...",
"requester": "oncall-alice",
"reason": "prod incident INC-1234",
"activated_at": "2026-03-27T12:00:00Z",
"expires_at": "2026-03-27T12:10:00Z",
"active": true
}

Request just-in-time elevated access.

  • Availability: Only registered when a JIT manager is configured.
  • Body:
{
"requester": "dev-bob",
"reason": "debugging issue #456",
"requested_role": "readwrite",
"ttl_seconds": 3600
}
  • Response: 201 Created — JSON access request object
{
"id": "...",
"requester": "dev-bob",
"reason": "debugging issue #456",
"requested_role": "readwrite",
"ttl_seconds": 3600,
"status": "pending",
"created_at": "2026-03-27T12:00:00Z"
}
  • Errors:
    • 413 Request Entity Too Large — body exceeds 1 MB
    • 400 Bad Request — invalid body or request validation failed

List pending JIT access requests.

  • Availability: Only registered when a JIT manager is configured.
  • Response: 200 OK
[
{
"id": "...",
"requester": "dev-bob",
"reason": "debugging issue #456",
"requested_role": "readwrite",
"status": "pending",
"created_at": "2026-03-27T12:00:00Z"
}
]

Approve a pending JIT access request.

  • Availability: Only registered when a JIT manager is configured.
  • Query Params: id (required) — the access request ID
  • Response: 200 OK — JSON approved access request object
  • Errors:
    • 400 Bad Request — missing id parameter
    • 404 Not Found — request not found or approval failed

Reject a pending JIT access request.

  • Availability: Only registered when a JIT manager is configured.
  • Query Params: id (required) — the access request ID
  • Response: 200 OK
{"status": "rejected"}
  • Errors:
    • 400 Bad Request — missing id parameter
    • 404 Not Found — request not found or rejection failed

List currently active JIT access sessions.

  • Availability: Only registered when a JIT manager is configured.
  • Response: 200 OK
[
{
"id": "...",
"requester": "dev-bob",
"requested_role": "readwrite",
"status": "approved",
"approved_at": "2026-03-27T12:05:00Z",
"expires_at": "2026-03-27T13:05:00Z"
}
]

Revoke an active JIT access session.

  • Availability: Only registered when a JIT manager is configured.
  • Query Params: id (required) — the access request ID
  • Response: 200 OK
{"status": "revoked"}
  • Errors:
    • 400 Bad Request — missing id parameter
    • 404 Not Found — revocation failed

Real-time event stream via WebSocket. Receives JSON-encoded events for query approvals, rejections, break-glass activations, and other system events.

  • Auth: Query param token must match ADMIN_API_KEY (when ADMIN_API_KEY is set)
  • Availability: Only registered when a WebSocket hub is configured.
  • Response: 101 Switching Protocols on success, 403 Forbidden on invalid token

All error responses use a consistent JSON format:

{"error": "<message>"}
CodeMeaning
200OK
201Created
400Bad Request — invalid input or missing required parameters
401Unauthorized — missing or invalid Bearer token
403Forbidden — authenticated but not allowed (RBAC)
404Not Found — resource does not exist
413Request Entity Too Large — body exceeds 1 MB
429Too Many Requests — rate limit exceeded (includes Retry-After: 60 header)
500Internal Server Error
503Service Unavailable — backend unreachable or feature not enabled

Cookie-based session login with the admin API key. Issued cookie: qg_session=<token>; HttpOnly; SameSite=Strict; Max-Age=28800.

  • Auth: Not required
  • Body: {"api_key": "<ADMIN_API_KEY>"}
  • Response: 200 OK {"status":"logged in"}
  • Errors: 401 Unauthorized — wrong key

Invalidate the session cookie.

  • Auth: Cookie session
  • Response: 200 OK {"status":"logged out"}

Return the current session’s authentication state and resolved role.

  • Auth: Not required
  • Response: 200 OK — e.g. {"authenticated":true,"role":"admin"}

Indicate whether OIDC SSO is configured (GATEKEEPER_OIDC_ISSUER set).

  • Auth: Not required
  • Response: 200 OK {"available":true} / {"available":false}

Initiate OIDC authorization flow. Always registered; the route is registered unconditionally so a Community user gets a license gate error instead of a confusing 404. Behavior:

  • License does not include oidc_sso403 Forbidden with {"error":"OIDC SSO requires an Enterprise license. See https://querycop.com/pricing"}
  • License is OK but GATEKEEPER_OIDC_ISSUER is not configured → 503 Service Unavailable with {"error":"OIDC SSO not configured ..."}
  • License + config OK → 302 Found redirect to the IdP authorization endpoint after generating a one-shot state parameter (stored in the session store).

OIDC redirect target. Always registered (same rationale as /auth/oidc/login). Returns the same 403 / 503 cases when license-disabled or unconfigured. On success: validates state, exchanges the authorization code, fetches userinfo / ID token claims, maps groups -> role via GATEKEEPER_OIDC_ROLE_MAP, and issues the qg_session cookie.


Return the active license status. Auth not required (intentional — the dashboard nudge UI shows tier / trial countdown / “Upgrade” link to anonymous visitors as part of the conversion funnel).

Heartbeat downgrade (license revocation or offline-grace timeout) is reflected in this response on the very next request: licenseProvider.Current() is the single read path, so /license and every Layer-2 license gate observe the same atomic flip.

  • Response: 200 OK
{
"tier": "pro",
"valid": true,
"is_trial": true,
"days_until_expiry": 7,
"in_grace_period": false,
"grace_days_left": 0,
"company": "ACME Inc.",
"features": ["jit","masking","custom_ai_prompt","session_recording"],
"all_features": ["oidc_sso","multi_tenant","session_recording","custom_ai_prompt","compliance_docs","masking","jit","plugin_sdk"],
"error": ""
}
  • features: feature keys this instance has enabled right now (matches pkg/license/license.go canonical names — custom_ai_prompt, session_recording, oidc_sso, compliance_docs, etc.; the pre-#72 aliases ai_policy_gen, session_playback, saml_sso, compliance_pdf are no longer emitted).
  • all_features: full Enterprise set so the dashboard can render disabled rows for unbought add-ons.
  • error: present when a heartbeat downgrade has flipped the tier to Community fallback; carries the downgrade reason (e.g. "license revoked: …"). Empty / absent in the steady state.

Cache: Cache-Control: no-store.


Routes that require a Pro or Enterprise feature emit a stable JSON 403 envelope when the license check fails. Clients (CLI, dashboard, custom integrations) can parse this uniformly to show upgrade hints:

HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "Data masking requires a Pro license",
"required_feature": "masking",
"upgrade_url": "https://querycop.com/pricing"
}
FieldNotes
errorHuman-readable message; ”
required_featureCanonical Feature constant from pkg/license/license.go (e.g. masking, jit, prometheus_metrics, oidc_sso). Use this to recognize the gate programmatically.
upgrade_urlAlways https://querycop.com/pricing.

The mapping of routes to feature constants is in the route registration matrix below. Heartbeat downgrade flips the gate result atomically: a client that succeeded a moment ago can start receiving this envelope without the server restarting.

Two documented exceptions to the canonical envelope:

  • GET /metrics returns 404 Not Found when prometheus_metrics is not licensed — by design, so unauthenticated scrapers can’t probe the licensing posture of an unlicensed instance.
  • GET /auth/oidc/login and /auth/oidc/callback return a plain 403 with {"error":"OIDC SSO requires an Enterprise license. See https://querycop.com/pricing"} (no required_feature / upgrade_url fields). These are browser-only redirect endpoints, not API surfaces.

Return the structured decision payload for a specific request id.

  • Query Params: id (required)
  • Response: 200 OK
{
"action":"UPDATE",
"decision":"require_human",
"risk_score":72,
"risk_reason":"WHERE clause missing",
"matched_policies":["role:junior_dev action:UPDATE"],
"requires_approval":true,
"db_user":"alice",
"database":"prod"
}
  • Errors: 400 missing id, 404 request not found.

Availability: Always registered — Core dry-run surface. License gate: none — /simulate is intentionally not gated by cicd_gate. The contrast with /api/v1/review (which IS Pro-gated) is by design: /review is the audited CI/CD surface, /simulate is the unmonitored “what if I ran this” surface for Community evaluation and ad-hoc exploration. The handler routes through interceptor.Simulate(...) directly and does not depend on the reviewer or AI analyzer being wired, so it works on a minimal install. The result is not recorded in the audit log.

  • Auth: Required when ADMIN_API_KEY is set (same as every other write endpoint; the (locked) convention was replaced in the heading with (Core dry-run) to clarify the license-gate status, not the auth status).
  • Body: {"sql":"SELECT 1"}
  • Max Body Size: 1 MB
  • Response: Same shape as /api/v1/review.

Slack App callback endpoint for the interactive approve/reject buttons. Only registered when GATEKEEPER_SLACK_SIGNING_SECRET is set.

  • Auth: Slack signature (X-Slack-Signature + X-Slack-Request-Timestamp), HMAC-SHA256 verified server-side. Replay window is 5 minutes.
  • Body: application/x-www-form-urlencoded Slack payload. action_id accepts both querycop_approve / querycop_reject and the legacy queryguard_approve / queryguard_reject for backward compat.
  • Response: 200 OK
  • Errors: 401 invalid signature, 400 malformed payload.

List recent recorded sessions (active in-memory + persisted JSONL when GATEKEEPER_SESSION_FILE is set).

  • Query Params: limit (int, default 100)
  • Response: 200 OK — JSON array of session metadata (id, db_user, database, client_addr, started_at, ended_at, duration, status, entry_count)

Return all recorded entries (queries / responses / approvals) for a single session id.

  • Errors: 400 missing id, 404 session not found.

Availability: All routes below are registered only when license.IsFeatureEnabled("jit") is true (Pro / Enterprise tier).

Submit a JIT elevation request.

  • Body: {"requester":"alice","role":"senior_dev","reason":"prod incident","ttl_seconds":3600}
  • Response: 201 Created — request object with id

List pending JIT requests.

Approve a JIT request and provision a temporary credential. Returns the generated username / password (one-shot; never repeated).

Reject a JIT request.

List currently active JIT sessions (granted + not yet revoked / expired).

Manually revoke an active JIT session before its TTL expires.


If an endpoint returns 404, it is most likely not registered because the corresponding subsystem is disabled. If it returns a license-shaped 403, the route is registered but the active license does not include the required feature. Both axes matter:

RoutesRegistered whenLicense gate (runtime)
/healthz /readyz / /auth/login /auth/logout /auth/me /auth/oidc/available /requests /approve /reject /explain /license /auditalways— (Core)
/auth/oidc/login /auth/oidc/callbackalways (E4-A2: registered unconditionally so the license + config errors are distinguishable)oidc_sso (plain 403, see exceptions above); 503 when license is OK but GATEKEEPER_OIDC_ISSUER is not configured
/metricsmetrics collector configured (always true in main wiring)prometheus_metrics (returns 404 when disabled, see exceptions above)
/policies (GET / PUT)policy engine configured
/wsWebSocket hub configured
/api/v1/reviewalways (E4-A3: registered unconditionally)cicd_gate (canonical 403 envelope); 503 when license is OK but reviewer is not wired
/api/v1/simulatealways— (intentionally ungated; Core dry-run surface routed through interceptor.Simulate(...), does not write to the audit log)
/masking/rules /masking/configmasking engine configuredmasking (canonical 403 envelope)
/breakglass/activate /breakglass/deactivate /breakglass/statusbreak-glass manager configuredbreak_glass (canonical 403 envelope)
/slack/interactionsGATEKEEPER_SLACK_SIGNING_SECRET setslack_approval (canonical 403 envelope)
/sessions /sessions/detailsession recorder configuredsession_recording (canonical 403 envelope)
/access/* (JIT)JIT manager configuredjit (canonical 403 envelope)

The License gate column reflects the per-request check (Layer 2): even if a route is registered at startup, a heartbeat downgrade flips it without restart. See “License-gated routes (403 envelope)” above for the response shape.