Querycop API Reference Community
Spec:
docs/openapi.yamlis the source of truth for the route surface (OpenAPI 3.1). CI runsgo run ./tools/check_openapi_routeswhich AST-parsespkg/api/handler.goand diffs the registered routes against the spec; any drift fails the build. Runmake check-openapilocally 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).
Infrastructure
Section titled “Infrastructure”GET /healthz
Section titled “GET /healthz”Liveness probe. Always returns 200 if the process is running.
- Auth: Not required
- Response:
200 OK
{"status": "ok"}GET /readyz
Section titled “GET /readyz”Readiness probe. Returns 200 if backend DB is reachable via TCP.
- Auth: Not required
- Response:
200 OKor503 Service Unavailable
Success:
{"status": "ready"}Failure:
{"status": "not ready", "reason": "backend unreachable"}GET /metrics
Section titled “GET /metrics”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
Section titled “Dashboard”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
Query Approval
Section titled “Query Approval”GET /requests (locked)
Section titled “GET /requests (locked)”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" }]POST /approve?id={id} (locked)
Section titled “POST /approve?id={id} (locked)”Approve a pending query.
- Query Params:
id(required) — the request ID - Response:
200 OK
{"status": "approved"}- Errors:
400 Bad Request— missingidparameter404 Not Found— request not found
POST /reject?id={id} (locked)
Section titled “POST /reject?id={id} (locked)”Reject a pending query and disconnect the client.
- Query Params:
id(required) — the request ID - Response:
200 OK
{"status": "rejected"}- Errors:
400 Bad Request— missingidparameter404 Not Found— request not found
Audit Log
Section titled “Audit Log”GET /audit (locked)
Section titled “GET /audit (locked)”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 containprev_hash/hashfields. To verify the tamper-evidence chain, read the JSONL file directly (seedocs/configuration.md§ 10) or usepkg/audit.VerifyChain. - Tier behavior: the in-memory window is Core — available on every tier, including Community.
MultiLogger.Queryis wired so that whenpersistent_auditis 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 Propersistent_auditfeature. - Query Params:
since— start time (RFC 3339 orYYYY-MM-DD)until— end time (RFC 3339 orYYYY-MM-DD)user— DB username filtertype— event type filter. One of:passthrough,blocked,approved,rejected,timeout,policy_deny,policy_allow,auto_approved,break_glass,data_guard_violationlimit— max entries to return (integer, default 100)
- Response:
200 OK— JSON array of audit entries - Errors:
400 Bad Request— invalidsince,until, orlimitparameter500 Internal Server Error— query failed503 Service Unavailable— audit log not enabled
RBAC Policy
Section titled “RBAC Policy”GET /policies (locked)
Section titled “GET /policies (locked)”Get current policy configuration as JSON.
- Availability: Only registered when a policy engine is configured.
- Response:
200 OK
{ "roles": [...], "rules": [...]}PUT /policies (locked)
Section titled “PUT /policies (locked)”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 MB400 Bad Request— failed to read request body500 Internal Server Error— policy load failed
SQL Review (CI/CD)
Section titled “SQL Review (CI/CD)”POST /api/v1/review (locked)
Section titled “POST /api/v1/review (locked)”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_gate→403 Forbiddenwith the canonical envelope (see “License-gated routes” below). - License is OK but no reviewer is wired (
WithReviewernot passed) →503 Service Unavailablewith{"error":"review service not configured"}. - License + reviewer OK → normal
200 OKwith the batch review result.
- License does not include
- 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 MB400 Bad Request— invalid request body or missingsqlfield500 Internal Server Error— review failed
Data Masking (Pro+)
Section titled “Data Masking (Pro+)”GET /masking/rules (locked)
Section titled “GET /masking/rules (locked)”Get all configured masking rules.
- Availability: Only registered when a masking engine is configured.
- Response:
200 OK
[ { "column": "email", "mask_type": "partial", "table": "users" }]POST /masking/rules (locked)
Section titled “POST /masking/rules (locked)”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 MB400 Bad Request— invalid body, missingcolumn, or missingmask_type
PUT /masking/config (locked)
Section titled “PUT /masking/config (locked)”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 MB400 Bad Request— invalid request body
Break-Glass (Pro+)
Section titled “Break-Glass (Pro+)”POST /breakglass/activate (locked)
Section titled “POST /breakglass/activate (locked)”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 MB400 Bad Request— invalid body or activation failed
POST /breakglass/deactivate (locked)
Section titled “POST /breakglass/deactivate (locked)”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 /breakglass/status (locked)
Section titled “GET /breakglass/status (locked)”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}JIT Access (Pro+)
Section titled “JIT Access (Pro+)”POST /access/request (locked)
Section titled “POST /access/request (locked)”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 MB400 Bad Request— invalid body or request validation failed
GET /access/requests (locked)
Section titled “GET /access/requests (locked)”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" }]POST /access/approve?id={id} (locked)
Section titled “POST /access/approve?id={id} (locked)”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— missingidparameter404 Not Found— request not found or approval failed
POST /access/reject?id={id} (locked)
Section titled “POST /access/reject?id={id} (locked)”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— missingidparameter404 Not Found— request not found or rejection failed
GET /access/active (locked)
Section titled “GET /access/active (locked)”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" }]POST /access/revoke?id={id} (locked)
Section titled “POST /access/revoke?id={id} (locked)”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— missingidparameter404 Not Found— revocation failed
WebSocket
Section titled “WebSocket”GET /ws?token={token}
Section titled “GET /ws?token={token}”Real-time event stream via WebSocket. Receives JSON-encoded events for query approvals, rejections, break-glass activations, and other system events.
- Auth: Query param
tokenmust matchADMIN_API_KEY(whenADMIN_API_KEYis set) - Availability: Only registered when a WebSocket hub is configured.
- Response:
101 Switching Protocolson success,403 Forbiddenon invalid token
Error Responses
Section titled “Error Responses”All error responses use a consistent JSON format:
{"error": "<message>"}Common HTTP Status Codes
Section titled “Common HTTP Status Codes”| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 400 | Bad Request — invalid input or missing required parameters |
| 401 | Unauthorized — missing or invalid Bearer token |
| 403 | Forbidden — authenticated but not allowed (RBAC) |
| 404 | Not Found — resource does not exist |
| 413 | Request Entity Too Large — body exceeds 1 MB |
| 429 | Too Many Requests — rate limit exceeded (includes Retry-After: 60 header) |
| 500 | Internal Server Error |
| 503 | Service Unavailable — backend unreachable or feature not enabled |
Authentication / Session
Section titled “Authentication / Session”POST /auth/login
Section titled “POST /auth/login”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
POST /auth/logout
Section titled “POST /auth/logout”Invalidate the session cookie.
- Auth: Cookie session
- Response:
200 OK{"status":"logged out"}
GET /auth/me
Section titled “GET /auth/me”Return the current session’s authentication state and resolved role.
- Auth: Not required
- Response:
200 OK— e.g.{"authenticated":true,"role":"admin"}
GET /auth/oidc/available
Section titled “GET /auth/oidc/available”Indicate whether OIDC SSO is configured (GATEKEEPER_OIDC_ISSUER set).
- Auth: Not required
- Response:
200 OK{"available":true}/{"available":false}
GET /auth/oidc/login
Section titled “GET /auth/oidc/login”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_sso→403 Forbiddenwith{"error":"OIDC SSO requires an Enterprise license. See https://querycop.com/pricing"} - License is OK but
GATEKEEPER_OIDC_ISSUERis not configured →503 Service Unavailablewith{"error":"OIDC SSO not configured ..."} - License + config OK →
302 Foundredirect to the IdP authorization endpoint after generating a one-shot state parameter (stored in the session store).
GET /auth/oidc/callback
Section titled “GET /auth/oidc/callback”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.
License
Section titled “License”GET /license
Section titled “GET /license”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 (matchespkg/license/license.gocanonical names —custom_ai_prompt,session_recording,oidc_sso,compliance_docs, etc.; the pre-#72 aliasesai_policy_gen,session_playback,saml_sso,compliance_pdfare 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.
License-gated routes (403 envelope)
Section titled “License-gated routes (403 envelope)”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 ForbiddenContent-Type: application/json
{ "error": "Data masking requires a Pro license", "required_feature": "masking", "upgrade_url": "https://querycop.com/pricing"}| Field | Notes |
|---|---|
error | Human-readable message; ” |
required_feature | Canonical Feature constant from pkg/license/license.go (e.g. masking, jit, prometheus_metrics, oidc_sso). Use this to recognize the gate programmatically. |
upgrade_url | Always 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 /metricsreturns404 Not Foundwhenprometheus_metricsis not licensed — by design, so unauthenticated scrapers can’t probe the licensing posture of an unlicensed instance.GET /auth/oidc/loginand/auth/oidc/callbackreturn a plain403with{"error":"OIDC SSO requires an Enterprise license. See https://querycop.com/pricing"}(norequired_feature/upgrade_urlfields). These are browser-only redirect endpoints, not API surfaces.
Decision Explanation
Section titled “Decision Explanation”GET /explain?id={id} (locked)
Section titled “GET /explain?id={id} (locked)”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:
400missing id,404request not found.
SQL Review (extra)
Section titled “SQL Review (extra)”POST /api/v1/simulate (Core dry-run)
Section titled “POST /api/v1/simulate (Core dry-run)”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_KEYis 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 Interactivity
Section titled “Slack Interactivity”POST /slack/interactions
Section titled “POST /slack/interactions”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-urlencodedSlack payload.action_idaccepts bothquerycop_approve/querycop_rejectand the legacyqueryguard_approve/queryguard_rejectfor backward compat. - Response:
200 OK - Errors:
401invalid signature,400malformed payload.
Sessions (recording)
Section titled “Sessions (recording)”GET /sessions (locked)
Section titled “GET /sessions (locked)”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)
GET /sessions/detail?id={id} (locked)
Section titled “GET /sessions/detail?id={id} (locked)”Return all recorded entries (queries / responses / approvals) for a single session id.
- Errors:
400missing id,404session not found.
JIT Access (Pro+)
Section titled “JIT Access (Pro+)”Availability: All routes below are registered only when
license.IsFeatureEnabled("jit") is true (Pro / Enterprise tier).
POST /access/request (locked)
Section titled “POST /access/request (locked)”Submit a JIT elevation request.
- Body:
{"requester":"alice","role":"senior_dev","reason":"prod incident","ttl_seconds":3600} - Response:
201 Created— request object with id
GET /access/requests (locked)
Section titled “GET /access/requests (locked)”List pending JIT requests.
POST /access/approve?id={id} (locked)
Section titled “POST /access/approve?id={id} (locked)”Approve a JIT request and provision a temporary credential. Returns the generated username / password (one-shot; never repeated).
POST /access/reject?id={id} (locked)
Section titled “POST /access/reject?id={id} (locked)”Reject a JIT request.
GET /access/active (locked)
Section titled “GET /access/active (locked)”List currently active JIT sessions (granted + not yet revoked / expired).
POST /access/revoke?id={id} (locked)
Section titled “POST /access/revoke?id={id} (locked)”Manually revoke an active JIT session before its TTL expires.
Route registration condition matrix
Section titled “Route registration condition matrix”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:
| Routes | Registered when | License gate (runtime) |
|---|---|---|
/healthz /readyz / /auth/login /auth/logout /auth/me /auth/oidc/available /requests /approve /reject /explain /license /audit | always | — (Core) |
/auth/oidc/login /auth/oidc/callback | always (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 |
/metrics | metrics collector configured (always true in main wiring) | prometheus_metrics (returns 404 when disabled, see exceptions above) |
/policies (GET / PUT) | policy engine configured | — |
/ws | WebSocket hub configured | — |
/api/v1/review | always (E4-A3: registered unconditionally) | cicd_gate (canonical 403 envelope); 503 when license is OK but reviewer is not wired |
/api/v1/simulate | always | — (intentionally ungated; Core dry-run surface routed through interceptor.Simulate(...), does not write to the audit log) |
/masking/rules /masking/config | masking engine configured | masking (canonical 403 envelope) |
/breakglass/activate /breakglass/deactivate /breakglass/status | break-glass manager configured | break_glass (canonical 403 envelope) |
/slack/interactions | GATEKEEPER_SLACK_SIGNING_SECRET set | slack_approval (canonical 403 envelope) |
/sessions /sessions/detail | session recorder configured | session_recording (canonical 403 envelope) |
/access/* (JIT) | JIT manager configured | jit (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.