API Reference¶
Base URL: http://localhost:3000 (default)
Most endpoints require authentication via Bearer token or API key:
OpenAPI Specification¶
DocBrain auto-generates an OpenAPI 3.1 specification from all API routes. The spec includes request/response schemas, authentication requirements, and error codes for every endpoint.
| Resource | URL |
|---|---|
| Swagger UI | GET /api/docs |
| OpenAPI JSON | GET /api/docs/openapi.json |
The Swagger UI is publicly accessible (no authentication required) and provides interactive API exploration with "Try it out" functionality. Use the OpenAPI JSON endpoint for client code generation with tools like openapi-generator, oapi-codegen, or swagger-codegen.
The specification is versioned — the info.version field tracks the DocBrain release version.
Authentication¶
Login¶
Exchange email + password for a session token. The token is a db_sk_... API key with a TTL set by LOGIN_SESSION_TTL_HOURS.
Request Body:
Response:
Logout¶
Revokes the current session token. Requires authentication.
Response: 200 OK with {"ok": true}
Verify Auth / Whoami¶
Returns the identity of the current token (API key or session key). Useful for verifying an API key is valid.
Response:
{
"key_id": "uuid",
"name": "Platform Team Key",
"role": "editor",
"allowed_spaces": ["PLATFORM", "SRE"],
"created_at": "2025-12-01T00:00:00Z"
}
Core Endpoints¶
Health Check¶
Returns 200 OK with {"status": "ok"}. Does not require authentication — used by load balancers and container health probes.
Ask a Question¶
Request Body:
{
"question": "How do I deploy to production?",
"session_id": "optional-uuid-for-conversation-continuity",
"space": "PLATFORM",
"spaces": ["PLATFORM", "SRE"],
"stream": true
}
session_id— optional UUID to continue a conversation across turnsspace— soft boost: results from this space get a 1.5× score multiplier but other spaces still appear. Use when you want cross-space results with your team's docs ranked first.spaces— hard filter: only return results from these spaces for this request. If combined with an API key'sallowed_spaces, the intersection is used (most restrictive wins). Omit to search all spaces.stream— iftrue, returns SSE; iffalse(default), returns JSON
Response (non-streaming):
{
"answer": "To deploy to production, follow these steps...",
"sources": [
{
"title": "Deployment Guide",
"heading": "Production Deployment",
"content": "...",
"source_url": "https://...",
"score": 0.92,
"freshness_score": 82.0,
"freshness_status": "fresh"
}
],
"session_id": "uuid",
"episode_id": "uuid",
"turn": 1,
"intent": "procedural",
"picker_trace_id": "uuid",
"user_failed_relevant": [],
"user_unconnected_relevant": ["jira.search"]
}
When live MCP tools are involved, the response carries three optional fields (all omitted when empty / not applicable):
picker_trace_id— present when MCP tools fired for this request. Pass it toGET /api/v1/ask/picker-trace/{request_id}to retrieve the full tool-selection trace (what was considered, selected, rejected, and why). Powers the "Why these tools?" explainability panel in the UI.user_failed_relevant— tool names the picker would have used, but the caller's OAuth token for that integration is expired/failed. Drives a "reconnect" prompt in the UI.user_unconnected_relevant— tool names the picker would have used, but the caller has not connected that integration at all. Drives a "connect this tool" prompt in the UI.
Streaming Response (stream: true):
Returns Server-Sent Events (SSE):
event: phase
data: {"status": "started", "phase": "retrieval", "description": "Searching documents..."}
event: phase
data: {"status": "completed", "phase": "retrieval", "duration_ms": 145, "result_count": 5}
event: token
data: {"text": "To deploy"}
event: token
data: {"text": " to production"}
event: answer
data: {"answer": "...", "sources": [...], "session_id": "...", "episode_id": "...", "intent": "procedural"}
GET /api/v1/ask/picker-trace/{request_id}¶
Returns the tool-picker trace for a recent /ask request — the catalog the
fast LLM saw, the tools it selected and rejected, its 1-sentence rationale,
and the user-specific buckets (user_unconnected_relevant,
user_failed_relevant) used to surface "connect this tool" hints in the UI.
Path params:
- {request_id} — the per-request id returned in the /ask response.
RBAC: any authenticated user. The handler restricts results to the
calling user — cross-user lookups return 404 (existence-disclosure
prevention). Service-account API keys (which have no user_id) get 403.
Response (200 OK):
{
"request_id": "uuid",
"user_id": "uuid",
"considered": [
{ "name": "github.issue_read", "manifest_id": "github" }
],
"selected": [
{ "name": "github.issue_read", "args": { "...": "..." } }
],
"rejected": [
{ "name": "jira.search", "reason": "not_relevant_to_query" }
],
"rationale": "Fetching the linked issue gives the deploy-rollback context.",
"user_unconnected_relevant": [
{ "manifest_id": "slack", "display_name": "Slack" }
],
"user_failed_relevant": []
}
Responses:
- 200 OK — trace returned.
- 403 Forbidden — caller is a service-account API key (no user_id).
- 404 Not Found — request_id unknown, expired, or owned by a different user.
Traces are held in an in-process per-user LRU; they expire after roughly one
hour or under cache pressure. The endpoint is read-only and does not write
to audit_log.
Generate a Document¶
Generate a documentation draft grounded in your org's own knowledge (corpus +
episodes + live MCP connectors), with per-claim provenance. Returns the markdown
— it does not publish. Stateless. Requires editor role (same auth as
/ask).
Each --source-url-style URL source is a named primary source: if any one
can't be fetched, the whole generation aborts (502) rather than producing a
doc from a subset.
Request Body (GenerateRequest):
{
"ask": "runbook for cert rotation",
"sources": [
{ "kind": "file", "label": "notes.md", "raw": "..." },
{ "kind": "url", "label": "https://acme.atlassian.net/wiki/spaces/OPS/pages/123", "raw": "" }
],
"target": "optional existing-doc reference to augment",
"template": "optional raw template file content (the bytes, not a path)",
"doc_type": "runbook",
"space": "OPS",
"no_enrich": false
}
ask— required. The natural-language description of the doc to write.sources— primary material.kindisfile,stdin, orurl. Forkind: "url", the URL goes inlabelandrawis""— the server fetches it via the connected MCP connector (Confluence page, Jira issue, Slack thread, GitHub PR or file).target— augment an existing doc rather than write from scratch.template— the raw template file content (not a path); shapes structure/tone only, can never disable a safety or quality rule.space— applies that Confluence space's quality rules;no_enrich: truedisables live-MCP enrichment.
Response (GeneratedArtifact):
{
"markdown": "# Cert Rotation Runbook\n...",
"doc_type": "runbook",
"provenance": [
{ "section": "Overview", "source_ids": ["chunk-1", "chunk-2"] }
],
"needs_input": [ "Which CA issues the production certs?" ],
"skipped_sources": [
{ "label": "jira", "reason": "connector not connected" }
],
"quality": {
"score": 87.0,
"violations": [ { "rule_name": "...", "severity": "warning", "message": "..." } ]
}
}
provenance— per-section attribution:{ section, source_ids }(sectionisnullfor whole-doc).needs_input— questions the doc can't answer from available knowledge (the honesty signal, not a fabrication).skipped_sources— sources that were unavailable, each{ label, reason }.quality—scoreis a 0–100 number; each violation is{ rule_name, severity, message }.
Error responses:
| Status | Meaning |
|---|---|
400 |
Validation failure / unknown source kind / unsupported or unrecognized URL (incl. a smuggled rule directive in a template) |
403 |
Caller is not an editor |
413 |
Source material over the per-source or aggregate size budget (inline or after fetching links) |
502 |
A named URL source could not be fetched (connector not connected/configured, or fetch error) |
503 |
Generation not configured |
See the Generate guide for the CLI, the template format, and CI playbooks.
Submit Feedback¶
Request Body:
feedback: 1 (helpful) or -1 (not helpful). Negative feedback seeds the Autopilot gap detection pipeline.
Freshness Report¶
GET /api/v1/freshness?space=DOCS
GET /api/v1/freshness?tags=architecture,api
GET /api/v1/freshness?archived=true
Query Parameters (mutually exclusive — archived > tags > space):
- space (optional) — Filter by document space
- tags (optional) — Comma-separated source labels. Returns docs whose source_labels overlap any value (e.g. ?tags=architecture,api)
- archived (optional) — When true, returns docs whose lifecycle_status is non-active (archived / reference / deprecated). Used to populate the "View excluded (N)" modal in the UI.
Response:
{
"space": "DOCS",
"summary": { "total_docs": 142, "fresh": 98, "review": 27, "stale": 12, "outdated": 5, "avg_score": 76.3 },
"documents": [
{
"document_id": "123",
"title": "API Guide",
"space": "DOCS",
"source_url": "https://...",
"total_score": 45.2,
"status": "stale",
"source_labels": ["api", "v2"],
"lifecycle_status": "active",
"time_decay_score": 30,
"engagement_score": 50,
"content_currency_score": 40,
"link_health_score": 60,
"contradiction_score": 80
}
]
}
Mark a Document Archived (Lifecycle Override)¶
Manually set a document's lifecycle status. Sticky — survives future syncs even if the source-system label changes. Requires admin.
status must be one of: active, archived, reference, deprecated. Setting active re-enables freshness scoring for the doc and triggers a rescore.
Response:
Backfill Lifecycle Across the Corpus¶
Re-derive lifecycle_status for every auto-managed doc from current source_labels and freshness.exclusion_rules config. Run after editing exclusion rules. Manual overrides are preserved. Requires admin.
Response:
Analytics¶
Query Parameters:
- days (optional, default: 30) — Reporting period in days
- space (optional) — Filter most-retrieved docs by Confluence space
- user_id (optional) — Filter query statistics by a specific user UUID
Response:
{
"period_days": 30,
"total_queries": 1247,
"unique_users": 45,
"avg_feedback": 0.82,
"unanswered_rate": 0.12,
"queries_by_day": [
{"date": "2025-01-15", "count": 42}
],
"top_intents": [
{"intent": "procedural", "count": 456}
],
"top_queries": [
{"query": "How do I deploy?", "count": 18}
],
"most_retrieved_docs": [
{
"title": "Deploy Guide",
"source_url": "https://...",
"space": "PLATFORM",
"retrieval_count": 94,
"freshness_score": 45.0,
"freshness_status": "stale"
}
]
}
CSV export:
Returns a CSV file of all query episodes in the period. Useful for external BI tools.
Analytics CSV Export¶
Returns a .csv file of all query episodes in the requested period. Columns: episode_id, created_at, user_id, query, intent, feedback, space.
Server Configuration¶
No authentication required. Returns enabled features and server version.
{
"version": "0.6.0",
"features": {
"freshness": true,
"analytics": true,
"slack": true,
"autopilot": true,
"incident_mode": true
}
}
Incident Mode¶
Request Body:
Activates incident mode, which prioritizes retrieval of runbooks and incident playbooks.
Admin Dashboard¶
Single-request admin overview. Returns all key health metrics in one parallel-fetched payload — designed for dashboards that need to avoid multiple round-trips.
Response:
{
"health": {
"total_documents": 342,
"overall_health_score": 67.3,
"freshness_distribution": {
"fresh": 120,
"review": 89,
"stale": 72,
"outdated": 41,
"archive": 20
},
"top_stale_cited_docs": [
{
"title": "Deploy Guide",
"freshness_score": 23.0,
"citations_last_7d": 47,
"contradiction_score": 45.0
}
],
"coverage_gaps": 15
},
"autopilot": {
"total_gaps": 20,
"open_gaps": 14,
"critical_gaps": 3,
"drafts_generated": 8,
"drafts_published": 2,
"last_analysis_at": "2025-02-20T14:30:00Z"
},
"forecast": {
"current_open_gaps": 14,
"projected_new_critical_30d": 5,
"projected_total_30d": 19,
"avg_weekly_new_gaps": 2.5,
"avg_weekly_resolved": 1.0,
"trend": "worsening"
},
"freshness_distribution": {
"fresh": 120,
"review": 89,
"stale": 72,
"outdated": 41,
"archive": 20
},
"top_gaps": [...],
"top_docs": [...],
"recent_audit": [
{
"action": "gap_dismissed",
"entity_id": "uuid",
"actor": "admin@example.com",
"created_at": "2026-02-25T10:00:00Z"
}
]
}
Autopilot Endpoints¶
Requires AUTOPILOT_ENABLED=true. All endpoints require authentication.
Autopilot Summary¶
Response:
{
"total_gaps": 20,
"open_gaps": 14,
"critical_gaps": 3,
"drafts_generated": 8,
"drafts_published": 2,
"last_analysis_at": "2025-02-20T14:30:00Z"
}
Gap Growth Forecast¶
Returns a 30-day projection of gap cluster growth based on the last 4 weeks of creation and resolution rates (linear extrapolation).
Response:
{
"current_open_gaps": 14,
"projected_new_critical_30d": 5,
"projected_total_30d": 19,
"avg_weekly_new_gaps": 2.5,
"avg_weekly_resolved": 1.0,
"trend": "worsening"
}
trend is one of:
- "improving" — resolution rate ≥ 75% of creation rate
- "stable" — resolution rate ≥ 40% of creation rate
- "worsening" — resolution rate < 40% of creation rate
List Gap Clusters¶
Query Parameters:
- limit (optional, default: 20) — Maximum clusters to return
- status (optional) — Filter by status: open, dismissed, resolved
- severity (optional) — Filter by severity: low, medium, high, critical
Response:
[
{
"id": "uuid",
"label": "Production Deployment Process",
"description": "Multiple questions about deploying services to production went unanswered.",
"query_count": 47,
"sample_queries": [
"How do I deploy to prod?",
"What's the canary process?",
"Where are the deployment configs?"
],
"avg_confidence": 0.28,
"severity": "critical",
"status": "open",
"unique_users": 12,
"negative_ratio": 0.68,
"trend": "recurring",
"assignee_id": null,
"assigned_at": null,
"created_at": "2025-02-15T10:00:00Z",
"updated_at": "2025-02-20T14:30:00Z"
}
]
New fields vs. earlier versions:
- unique_users — distinct users who hit this gap
- negative_ratio — fraction of queries on this topic with negative feedback
- trend — "new" (appeared in last 7 days) or "recurring" (open > 7 days)
- assignee_id — UUID of the user assigned to resolve this gap, or null
- assigned_at — ISO timestamp when the gap was assigned, or null
Trigger Gap Analysis¶
Runs gap analysis immediately (normally runs on the AUTOPILOT_GAP_ANALYSIS_INTERVAL_HOURS schedule). Returns the number of new clusters created.
Response:
Dismiss a Gap¶
Marks a gap cluster as dismissed (not worth addressing). Requires admin or editor role.
Assign a Gap¶
Assigns a gap cluster to a user for resolution.
Request Body:
Response: 200 OK with the updated gap cluster object.
Gap Related Documents¶
Returns documents semantically related to this gap cluster — these are the docs that users were trying to get answers from when the gap was detected. Useful for identifying which authors to notify or which content needs updating.
Response:
[
{
"source_id": "doc-123",
"title": "Production Deployment Guide",
"source_url": "https://confluence.example.com/...",
"space": "PLATFORM",
"freshness_score": 23.5,
"author": "author@example.com"
}
]
List Drafts¶
Query Parameters:
- status (optional) — Filter by status: pending_review, approved, published, rejected
- limit (optional, default: 20) — Maximum drafts to return
Response:
[
{
"id": "uuid",
"cluster_id": "uuid",
"title": "Production Deployment Runbook",
"content": "# Production Deployment\n\n## Prerequisites\n...",
"content_type": "runbook",
"source_queries": ["How do I deploy to prod?", "..."],
"source_doc_ids": ["doc-uuid-1", "doc-uuid-2"],
"quality_score": 0.87,
"status": "pending_review",
"created_at": "2025-02-20T15:00:00Z"
}
]
Get a Draft¶
Returns full draft content for review.
Generate Draft for a Gap¶
Generates a draft document for the specified gap cluster. Uses existing docs as context. Also DMs Slack authors of related docs if SLACK_BOT_TOKEN is configured.
Response:
{
"draft_id": "uuid",
"title": "Production Deployment Runbook",
"content_type": "runbook",
"quality_score": 0.87
}
Update Draft Status¶
Request Body:
Valid statuses: approved, published, rejected
Publish Draft¶
Publishes an approved draft to the configured target system. Creates a page (Confluence), PR (GitHub), or MR (GitLab) with the draft content.
Query Parameters:
- target (optional) — Override the publish target: confluence, github, gitlab. If omitted, uses per-space routing (DB) → default config target.
Response:
{
"draft_id": "uuid",
"status": "published",
"target": "github",
"url": "https://github.com/acme/docs/pull/42"
}
Auth: Editor role or above.
Doc-Improvement Evidence¶
Returns the labeled evidence chains for auto-published fixes plus an honest aggregate. Each chain reports how far a fix progressed along the proven path — published → content-changed → re-ingest-confirmed → human-approved → measured freshness/quality delta — with every link at its true strength (proven, pending, weak, or not_applicable). There is no single "improved" boolean; intent is never reported as outcome, and a freshness/quality delta is shown only when it was actually measurable.
A published fix not confirmed live within the configured re-ingest-confirm timeout (IMPROVEMENT_REINGEST_CONFIRM_TIMEOUT_HOURS, default 72 hours) is reported on its re-ingest link as weak with the label "stale — published but never confirmed live", distinguishing a stuck publish from a normal in-flight one (labelled "published, not yet confirmed live").
Query Parameters:
- limit (optional, default 50) — Maximum number of chains to return, newest first. Clamped to a server-side maximum.
Response:
{
"chains": [
{
"draft_id": "uuid",
"target_document_id": "uuid",
"proven_depth": 2,
"links": [
{ "key": "published", "state": "proven", "label": "published toward gap", "detail": null },
{ "key": "content_changed", "state": "pending", "label": "awaiting re-ingest", "detail": null },
{ "key": "reingest_confirmed", "state": "weak", "label": "stale — published but never confirmed live", "detail": null },
{ "key": "human_approved", "state": "proven", "label": "approved by human review", "detail": null },
{ "key": "delta", "state": "not_applicable", "label": "delta not measurable for this fix", "detail": null }
]
}
],
"aggregate": {
"published": 12,
"reingest_confirmed": 7,
"human_approved": 5,
"measured_improvement": 3
}
}
Auth: Admin role.
Publish Targets¶
Manage per-space publish target routing. Admin-only endpoints.
List Publish Targets¶
Response:
[
{
"id": "uuid",
"space": "PLATFORM",
"target_type": "github",
"config": {"repo": "acme/platform-docs", "token_env": "GITHUB_PUBLISH_TOKEN"},
"priority": 10,
"created_at": "2026-03-25T00:00:00Z",
"updated_at": "2026-03-25T00:00:00Z"
}
]
Create Publish Target¶
Request Body:
{
"space": "PLATFORM",
"target_type": "github",
"config": {
"repo": "acme/platform-docs",
"token_env": "GITHUB_PUBLISH_TOKEN",
"branch": "main",
"docs_path": "docs"
},
"priority": 10
}
space— Space key to route (omit ornullfor default/fallback target)target_type—confluence,github, orgitlabconfig— JSONB target configuration. Must usetoken_env(env var name) instead of raw secrets. Fields containingtoken,api_token, orsecretare rejected.priority— Higher priority targets are chosen first (default: 0)
Response: 201 Created with the created target.
Errors:
- 400 — Invalid target type or raw secrets in config
- 409 — A target for this space + type already exists
Update Publish Target¶
Partial update — only provided fields are changed.
Request Body:
Response: 200 OK with the updated target.
Delete Publish Target¶
Response: 204 No Content
Weekly Digest Preview¶
Returns the current weekly digest data (without sending it to Slack).
Response:
{
"period_start": "2025-02-13T00:00:00Z",
"period_end": "2025-02-20T00:00:00Z",
"total_queries": 152,
"unanswered_queries": 18,
"top_gaps": [
{
"id": "uuid",
"label": "Production Deployment Process",
"query_count": 47,
"severity": "critical",
"trend": "recurring"
}
],
"new_drafts": [...],
"stale_doc_count": 7,
"top_docs_by_queries": [
{
"title": "API Rate Limits",
"source_url": "https://...",
"space": "PLATFORM",
"retrieval_count": 94,
"author": "author@example.com"
}
]
}
top_docs_by_queries — the 10 documents most frequently retrieved during the period, with author attribution. Gives doc owners insight into which content is being searched most heavily.
Knowledge Health¶
Health Report¶
Full knowledge base health overview.
Response:
{
"total_documents": 342,
"overall_health_score": 67.3,
"freshness_distribution": {
"fresh": 120,
"review": 89,
"stale": 72,
"outdated": 41,
"archive": 20
},
"top_stale_cited_docs": [
{
"title": "Deploy Guide",
"freshness_score": 23.0,
"citations_last_7d": 47,
"contradiction_score": 45.0
}
],
"coverage_gaps": 15
}
Admin Endpoints¶
All admin endpoints require an admin-role API key.
List API Keys¶
Create API Key¶
Request Body:
role: viewer, editor, analyst, admin
viewer— ask questions, browse answers, give feedback, and access all intelligence dashboards (Documentation Analytics, Predictive Gaps, Autonomous Document Maintenance, Knowledge Stream)editor— everything viewer can + manage spaces and capturesanalyst— everything editor can; reserved for future role-based scoping, currently equivalent toeditoradmin— full access including user management, RBAC config, and ingest triggers
allowed_spaces: hard-filters all queries and ingestion to the listed spaces. Empty array = no restriction.
Revoke API Key¶
Onboarding Mode¶
Returns an AI-curated reading list for a new team member.
Query Parameters:
- role — Job role or persona (e.g. platform-engineer, sre, backend-developer)
- days (optional, default: 7) — Onboarding period in days
Response:
{
"role": "platform-engineer",
"reading_list": [
{
"title": "Platform Onboarding Guide",
"source_url": "https://...",
"freshness_score": 85.0,
"reason": "Direct onboarding guide covering role-specific processes and expectations."
}
]
}
Admin — MCP Manifests¶
Admin endpoints for inspecting and operating the MCP tool platform — viewing merged tool catalogs, forcing discovery probes, and managing OAuth probe-user designations for dynamic manifests.
All endpoints require the admin role.
GET /api/v1/admin/mcp/manifests/{id}¶
Get full manifest detail including the merged tool catalog and discovery status.
Response (200 OK):
{
"id": "github",
"active_version": 5,
"display_name": "GitHub",
"auth": { "...": "..." },
"secrets": [ "..." ],
"tools": [
{
"name": "github.issue_read",
"description": "...",
"tool_source": "discovered",
"read_only": true,
"args_schema": { "...": "..." },
"output_size_cap_bytes": 16384,
"latency_budget_ms": 7000,
"upstream_name": "issue_read"
}
],
"discovery": {
"mode": "dynamic",
"refresh_seconds": 3600,
"status": "ok",
"last_attempt": "2026-05-15T20:42:11Z",
"last_success": "2026-05-15T20:42:11Z",
"last_error": null,
"next_scheduled": null,
"collisions": []
},
"probe_user": {
"user_id": "uuid",
"designated_at": "ISO-8601",
"designated_by": "uuid",
"last_probed_at": "ISO-8601|null"
}
}
Field semantics:
tools[].tool_source— one ofstatic|discovered|static_override. Tools are merged from both sources; collisions surface indiscovery.collisions.discovery.mode—static|dynamic|unknown(the last when the manifest isn't in the registry, e.g. after a discovery-disabled rollback).discovery.status—not_applicable|pending|ok|failed|requires_probe_user|degraded_collisions.discovery.last_success— populated forokanddegraded_collisions(degraded means the probe succeeded with name collisions; it is not a probe failure).discovery.collisions[]— names that appear in both static and discovered catalogs when neither side hasoverride_discovered: true.probe_user— omitted entirely when no designation exists.
For static manifests discovery.mode == "static" and all other discovery
fields are null. Every tool has tool_source: "static".
POST /api/v1/admin/mcp/manifests/{id}/discover¶
Force an immediate probe of a dynamic MCP manifest. Synchronously runs the probe and returns the new catalog, or surfaces the failure inline so the admin UI can render it.
Path params:
- {id} — the manifest_id.
Responses:
- 200 OK — { "outcome": "ok", "count": N, "tools": [...], "probed_at": "ISO-8601" }
- 200 OK — { "outcome": "failed", "error": { "kind": "...", "detail": "..." }, "probed_at": "ISO-8601" }
- 404 Not Found — manifest_id not in registry
- 409 Conflict — manifest is static, OR a probe is already in flight for this manifest
- 503 Service Unavailable — discovery worker not configured
- 504 Gateway Timeout — probe did not complete within 7s
- 500 Internal Server Error — worker dropped the reply channel (bug indicator)
error.kind is a stable snake_case token: timeout | auth | http |
parse | transport | catalog_too_large | not_dynamic | not_found |
requires_probe_user | already_in_flight. The UI can branch on this; detail
is human-readable.
Audit: writes mcp.manifest.discover to audit_log with {outcome, count}
on success or {outcome, error_kind, error_detail} on failure.
GET /api/v1/admin/mcp/manifests/{id}/probe-user¶
Read the current OAuth probe-user designation. Returns the user designated to provide OAuth credentials for periodic discovery probes on this manifest.
Responses:
- 200 OK —
{
"manifest_id": "github",
"user_id": "uuid",
"designated_at": "ISO-8601",
"designated_by": "uuid",
"last_probed_at": "ISO-8601|null"
}
404 Not Found — no probe user designated for this manifest
Unlike the other probe-user endpoints, this read succeeds even when MCP discovery is disabled, so admins can audit historical designations after a rollback.
PUT /api/v1/admin/mcp/manifests/{id}/probe-user¶
Designate a user as the OAuth probe credential source. Validates that the user
has a non-revoked, non-expired OAuth token for this manifest before recording
the designation. The discovery worker will use this user's token for periodic
tools/list probes.
Request: { "user_id": "uuid" }
Responses:
- 204 No Content — designation recorded
- 409 Conflict — user has no valid OAuth token for this manifest (the user
must connect first via the standard OAuth flow)
- 503 Service Unavailable — discovery worker not configured
Audit: writes mcp.manifest.probe_user.set with {user_id}.
DELETE /api/v1/admin/mcp/manifests/{id}/probe-user¶
Remove the OAuth probe-user designation. After unset, the manifest's discovery
status flips to requires_probe_user on the next probe tick — no probes will
run until a new user is designated.
Responses:
- 204 No Content — designation removed (or was already absent)
- 503 Service Unavailable — discovery worker not configured
Audit: writes mcp.manifest.probe_user.unset with {prior_user_id} (the
user being un-designated, captured pre-delete for audit completeness).
GET /api/v1/admin/principals¶
Search the principals table by case-insensitive prefix match on display name or external id. Powers the admin UI's principal typeahead (e.g. when scoping a tool to an SSO group or user).
Query params:
- q — prefix to match against display and external_id.
- limit — max results, clamped to 1..=100 (default 25).
Response (200 OK):
{
"principals": [
{
"id": 3,
"kind": "sso_group",
"source": "sso",
"external_id": "engineering",
"display": "Engineering"
}
]
}
GET /api/v1/admin/mcp/manifests/{id}/usage¶
Aggregate the MCP audit log for a manifest over a rolling window. Powers the admin UI Usage dashboard.
Query params:
- days — window size, clamped to 1..=90 (default 7).
Response (200 OK):
{
"series": [{ "day": "2026-05-19", "outcome": "ok", "count": 12 }],
"top_users": [{ "user_id": "uuid", "display": "Alice", "count": 9 }],
"top_failures": [{ "tool_name": "jira.search", "args": {}, "error_class": "timeout", "count": 3 }]
}
Field semantics:
- series[] — daily invocation counts grouped by outcome.
- top_users[] — top 10 attributable users by invocation count.
- top_failures[] — top 5 failing (tool_name, error_class) groups, each with
a redacted-args sample.
POST /api/v1/admin/mcp/manifests/{id}/disable¶
Reversibly disable a manifest. Sets all of the manifest's enablements to disabled, removing it from tool dispatch while preserving its scope configuration — so re-enabling is a single step. Idempotent.
Responses:
- 200 OK — manifest disabled (or was already disabled).
Audit: writes a manifest-disable entry to audit_log.
DELETE /api/v1/admin/mcp/manifests/{id}¶
Irreversibly uninstall a manifest in a single transaction. Clears the active-version pointer, then removes the manifest's OAuth tokens, probe users, secrets, enablements, and all installed versions. Per-user OAuth tokens are deleted — users must re-authorize on reinstall.
Responses:
- 200 OK — manifest uninstalled.
- 404 Not Found — manifest not installed.
Audit: writes a manifest-uninstall entry to audit_log.
Admin — MCP Registry & Install¶
Admin endpoints for browsing the signed remote MCP registry, fetching a
single manifest from it, and installing a manifest in a single transactional
call. All endpoints require the admin role.
The 3 registry endpoints (GET /registry, GET /registry/{id}/manifest,
POST /install-from-registry) require MCP_REGISTRY_PUBKEY to be set at
boot. When unset the server boots normally and these endpoints return
503 Service Unavailable; admins can still install manifests via the
existing paste/URL flow.
GET /api/v1/admin/mcp/registry¶
List the cached signed registry index.
The server fetches the index from MCP_REGISTRY_URL, verifies its Ed25519
signature against MCP_REGISTRY_PUBKEY, and caches it on disk at
MCP_REGISTRY_CACHE_PATH so subsequent calls survive transient network
outages (Tier 2 fallback).
Response (200 OK):
{
"schema_version": 1,
"generated_at": "2026-05-15T20:42:11Z",
"entries": [
{
"id": "github",
"version": "1.4.2",
"display_name": "GitHub",
"summary": "Read-only GitHub MCP — issues, PRs, code search.",
"manifest_url": "https://registry.docbrain-ai.com/v1/github/1.4.2.yaml",
"manifest_sha256": "abc123…",
"signed": true
}
]
}
Responses:
- 200 OK — index returned (from network or disk cache).
- 502 Bad Gateway — both network fetch and disk cache failed (cold start
with no connectivity).
- 503 Service Unavailable — MCP_REGISTRY_PUBKEY not configured.
GET /api/v1/admin/mcp/registry/{id}/manifest¶
Fetch and verify a single manifest by registry id. Used by the install wizard to preview the manifest before committing.
Path params:
- {id} — registry entry id (e.g. github).
Response (200 OK):
{
"yaml": "id: github\ndisplay_name: GitHub\n...",
"entry": {
"id": "github",
"version": "1.4.2",
"display_name": "GitHub",
"manifest_url": "https://registry.docbrain-ai.com/v1/github/1.4.2.yaml",
"manifest_sha256": "abc123…",
"signed": true
}
}
Responses:
- 200 OK — manifest YAML + index entry returned.
- 404 Not Found — id not present in the registry index.
- 502 Bad Gateway — manifest fetch failed or sha256 / signature
verification failed.
- 503 Service Unavailable — MCP_REGISTRY_PUBKEY not configured.
POST /api/v1/admin/mcp/install-from-registry¶
Atomic install of a registry manifest. Fetches and verifies the manifest,
validates its schema, then in a single transaction writes
mcp_installed_manifests, sets mcp_active_manifest_version, creates
mcp_manifest_enablements for the requested scope, and records an
audit_log row with action admin_mcp_install_from_registry.
Request:
Or with a group scope:
{
"id": "github",
"version": "1.4.2",
"allow_unsigned": false,
"scope": { "type": "groups", "group_ids": ["uuid1", "uuid2"] }
}
allow_unsigned— MUST befalse. Unsigned installs are not supported by this endpoint; use the existing paste/URL install endpoint instead (which renders its own per-manifest opt-in audit row).scope.type—everyoneorgroups. Whengroups,group_idsis required.
Response (200 OK):
Idempotent: re-installing the same (id, version) pair returns 200 with
already_installed: true and does not duplicate audit rows.
Responses:
- 200 OK — installed (or already present).
- 400 Bad Request — allow_unsigned: true, malformed scope, or schema
validation failure on the fetched manifest.
- 404 Not Found — id not present in the registry index.
- 502 Bad Gateway — manifest fetch / verification failed.
- 503 Service Unavailable — MCP_REGISTRY_PUBKEY not configured.
Audit: writes admin_mcp_install_from_registry with {id, version,
scope, enabled_count} on success.
GET /api/v1/admin/mcp/secrets/audit/{manifest_id}¶
Inspect the running pod's environment for the env-var keys declared in
the manifest's service_account.secret_refs and render the kubectl
command to patch any missing ones.
Path params:
- {manifest_id} — the active manifest to audit.
Response (200 OK):
{
"manifest_id": "github",
"required": ["GITHUB_TOKEN", "GITHUB_APP_ID"],
"missing": ["GITHUB_APP_ID"],
"secret_name": "docbrain-secrets",
"namespace": "docbrain",
"patch_command": "kubectl create secret generic docbrain-secrets --from-literal=GITHUB_APP_ID=<PASTE_VALUE> --dry-run=client -o yaml | kubectl apply -f -"
}
When DOCBRAIN_K8S_SECRET_NAME or DOCBRAIN_K8S_NAMESPACE are unset, the
rendered command contains the placeholder strings <set
DOCBRAIN_K8S_SECRET_NAME> / <set DOCBRAIN_K8S_NAMESPACE> so the admin
sees exactly what to configure.
Operability: env vars inject once at container start. After applying
the rendered patch the admin MUST restart the pod (e.g. kubectl rollout
restart deployment/docbrain-server) for the audit endpoint to reflect
the new values.
Responses:
- 200 OK — audit result returned.
- 404 Not Found — manifest_id unknown.
POST /api/v1/admin/mcp/secrets/oauth¶
Reserved for v2 — programmatic OAuth-client-secret install via the Kubernetes API.
Response (501 Not Implemented):
{
"error": "not_implemented",
"hint": "Use the kubectl command rendered by GET /api/v1/admin/mcp/secrets/audit/{manifest_id} until v2 wires the Kubernetes client.",
"kind": "v1_kubectl_only"
}
Admin — Ownership Accuracy Audit¶
POST /api/v1/admin/ownership/audit¶
Run an accuracy audit over the labeled ownership set. This evaluates how often the expertise scorer's confident attributions are wrong, and derives the confidence cutoff that meets a target error rate — the evidence operators use to decide whether to open the UI accuracy gate. Admin role required.
Request body:
-target_error — the maximum confidently-wrong rate to target when deriving the confidence cutoff (e.g. 0.0–1.0).
- split_frac — the calibration/test split fraction; clamped internally to [0.1, 0.9].
Response (200): an accuracy summary containing the confident and abstain totals, the measured confidently-wrong rate, the area under the risk-coverage curve, the full risk-coverage curve, and the calibrate-then-test report.
Knowledge Graph¶
GET /api/v1/graph/entity/{name}¶
Disambiguate an entity by name and return its subgraph (neighbors and edges).
Response:
{
"ranked": [
{
"entity": { "id": "uuid", "name": "payments-service", "entity_type": "service" },
"score": 0.95,
"reason": "direct_connections=2, degree=15"
}
],
"subgraph": {
"nodes": [
{ "id": "uuid", "name": "payments-service", "entity_type": "service" }
],
"edges": [
{ "from_entity_id": "uuid1", "to_entity_id": "uuid2", "relation_type": "DEPENDS_ON" }
]
}
}
GET /api/v1/graph/dependencies/{entity_id}¶
Multi-hop dependency traversal from a given entity.
Query params: depth (default 2, max 5), direction (downstream | upstream | both), relation_types (comma-separated, optional)
Response:
[
{
"entity": { "id": "uuid", "name": "auth-service", "entity_type": "service" },
"depth": 1,
"path": ["uuid1", "uuid2"]
}
]
GET /api/v1/graph/blast-radius/{entity_id}¶
Determine what is affected if an entity changes or goes down.
Query params: depth (default 3, max 5)
Response:
{
"entity": { "id": "uuid", "name": "payments-service", "entity_type": "service" },
"affected": [{ "entity": { "id": "uuid", "name": "checkout-service", "entity_type": "service" }, "depth": 1, "path": [] }],
"by_type": { "service": [{ "id": "uuid", "name": "checkout-service", "entity_type": "service" }] },
"by_depth": { "1": [{ "id": "uuid", "name": "checkout-service", "entity_type": "service" }] }
}
GET /api/v1/graph/path¶
Find the shortest path between two entities.
Query params: from (UUID, required), to (UUID, required), depth (max hops, default 5)
Response: Array of GraphEdge objects, or null if no path found within depth.
GET /api/v1/graph/experts/{topic}¶
Route to domain experts via the entity-to-team-to-person chain.
Response:
[
{
"person": { "id": "uuid", "name": "Alice Smith", "entity_type": "person" },
"team": { "id": "uuid", "name": "platform-team", "entity_type": "team" },
"confidence": 0.85,
"route": []
}
]
Documentation Analytics¶
GET /api/v1/analytics/velocity¶
Org-wide documentation analytics metrics over a configurable time window.
Query params: days (default 30)
Response:
{
"current_velocity": 2.5,
"velocity_trend": "accelerating",
"grade": "B",
"knowledge_half_life_days": 45,
"tribal_knowledge_pct": 0.15,
"documentation_roi": {
"queries_deflected": 340,
"estimated_hours_saved": 85
},
"weekly_snapshots": [
{ "week": "2026-03-09", "velocity": 2.3, "docs_created": 4, "gaps_resolved": 2 }
],
"per_team": [{ "team": "platform", "velocity": 3.2, "grade": "A" }]
}
GET /api/v1/analytics/velocity/teams¶
Per-team velocity breakdown.
Query params: days (default 30)
GET /api/v1/analytics/velocity/roi¶
Org-wide ROI summary: total queries deflected, hours saved, and cost saved in USD over the selected time window.
Query params: days (default 30)
Response:
Predictive Gap Detection¶
POST /api/v1/predictive/code-change¶
Detect documentation that may be stale after a code change.
Request:
Response: Array of PredictedGap objects with doc_id, title, reason, confidence, trigger.
GET /api/v1/predictive/cascade¶
Detect cascade staleness — documents that reference recently-updated documents and may now be inconsistent.
GET /api/v1/predictive/seasonal¶
Detect seasonal query patterns approaching their predicted peak for proactive refresh.
GET /api/v1/predictive/onboarding¶
Detect onboarding gaps — common questions from new hires that are poorly covered or missing from documentation.
Doc Maintenance¶
GET /api/v1/maintenance/fixes¶
List auto-detected fix proposals (contradictions, broken links, version bumps).
Query params: doc_id (optional), status (pending | approved | applied | rejected), limit (default 50)
POST /api/v1/maintenance/fixes/{id}/apply¶
Apply a fix proposal. Requires authentication.
Response: 200 OK
POST /api/v1/maintenance/fixes/{id}/reject¶
Reject a fix proposal. Requires authentication.
Response: 200 OK
GET /api/v1/maintenance/stats¶
Aggregate fix proposal statistics.
Response:
Knowledge Stream¶
GET /api/v1/stream/events¶
List recent stream events (incidents, decay alerts, expertise gaps, doc updates).
Query params: since (RFC3339), type (incident_warning | decay_alert | expertise_gap | doc_updated), limit (default 50)
GET /api/v1/stream/events/user/{user_id}¶
Personalized event stream filtered by the user's active context.
POST /api/v1/stream/context¶
Update user context (services and topics) for personalized stream delivery.
Request:
GET /api/v1/stream/stats¶
Event count statistics broken down by time window (24h, 7d, 30d) and event type.
Event Bus¶
GET /api/v1/events¶
Query the persistent event log. Requires admin role.
Query params:
| Param | Type | Default | Description |
|---|---|---|---|
type |
string | — | Filter by event type (e.g. gap.detected, document.ingested) |
since |
string | — | RFC3339 datetime or YYYY-MM-DD — only events after this time |
limit |
integer | 100 |
Max results (1–1000) |
offset |
integer | 0 |
Pagination offset |
Response:
{
"events": [
{
"id": "uuid",
"event_type": "gap.detected",
"payload": { "cluster_id": "uuid", "severity": "critical", "label": "...", "query_count": 15, "unique_users": 8 },
"emitted_at": "2026-03-21T14:30:00Z",
"processed_by": ["event_logger"]
}
],
"count": 1,
"limit": 100,
"offset": 0
}
Event types: document.ingested, document.updated, document.deleted, freshness.changed, quality.scored, fragment.captured, fragment.indexed, fragment.promoted, gap.detected, gap.assigned, gap.resolved, draft.generated, draft.review_requested, draft.published, draft.rejected, query.answered, feedback.received, sla.breached, maintenance.fix_proposed
GET /api/v1/events/stream¶
SSE stream of real-time events. Requires admin role. Max 10 concurrent connections.
Each SSE message includes:
- event: — the event type (e.g. gap.detected)
- id: — unique event UUID (for Last-Event-ID reconnection)
- data: — JSON payload with the full EventEnvelope (id, event, emitted_at)
Knowledge Fragments¶
POST /api/v1/fragments¶
Create a new knowledge fragment. Requires editor role.
Fragments are routed by confidence: >= auto_index_threshold (default 0.7) → auto-indexed into search; >= review_threshold (default 0.4) → queued for review; below → auto-discarded.
Request body:
{
"fragment_type": "decision",
"summary": "Switched from Redis pub/sub to PG LISTEN/NOTIFY",
"content": "Redis cluster mode doesn't support pub/sub across shards...",
"source_type": "pr_merge",
"source_ref": "https://github.com/acme/platform/pull/1234",
"source_id": "github:acme/platform#1234",
"confidence": 0.85,
"space": "PLATFORM",
"related_doc_ids": ["550e8400-e29b-41d4-a716-446655440000"],
"code_location": "src/events/publisher.rs:42"
}
| Field | Type | Required | Description |
|---|---|---|---|
fragment_type |
string | yes | decision, fact, caveat, procedure, context |
summary |
string | yes | Short description |
content |
string | yes | Full content (max FRAGMENT_MAX_CONTENT_LENGTH) |
source_type |
string | yes | pr_merge, commit, ide_annotation, conversation_distill, deploy, incident, manual, ci_analyze |
source_ref |
string | no | URL or reference to the source |
source_id |
string | no | Dedup key (unique per source_type) |
confidence |
float | no | 0.0–1.0, default 0.5 |
space |
string | no | Space for routing/filtering |
related_doc_ids |
UUID[] | no | Related document IDs |
code_location |
string | no | File path and line (e.g. src/foo.rs:42) |
Response: 201 Created
GET /api/v1/fragments¶
List fragments with optional filters.
| Param | Type | Default | Description |
|---|---|---|---|
status |
string | — | Filter by status: pending, indexed, promoted, discarded, review_queued |
space |
string | — | Filter by space |
source_type |
string | — | Filter by source type |
limit |
integer | 50 |
Max results (1–1000) |
offset |
integer | 0 |
Pagination offset |
GET /api/v1/fragments/:id¶
Get a single fragment by ID.
PATCH /api/v1/fragments/:id¶
Update a fragment. Requires editor role. Only provided fields are updated.
DELETE /api/v1/fragments/:id¶
Delete a fragment. Requires admin role. Also removes from search index.
GET /api/v1/fragments/review-queue¶
List fragments with review_queued status. Requires analyst role. Supports same filters as list.
POST /api/v1/fragments/:id/approve¶
Approve a fragment — sets status to indexed and embeds/indexes into OpenSearch. Requires analyst role.
POST /api/v1/fragments/:id/discard¶
Discard a fragment with optional reason. Requires analyst role.
Request body:
GET /api/v1/fragments/stats¶
Fragment statistics — counts by status, source type, and space. Supports ?space= filter.
GET /api/v1/fragments/clusters¶
Discover semantic clusters of related fragments. Uses embedding similarity to group fragments by topic. Requires analyst role.
Returns clusters sorted by composability score (highest first). Clusters with composable: true meet the composition threshold (5+ fragments, diverse sources, 500+ words).
Response:
[
{
"topic": "Redis Cluster Mode Limitations",
"fragment_ids": ["uuid1", "uuid2", "uuid3", "uuid4", "uuid5"],
"source_diversity": 3,
"author_diversity": 4,
"total_content_words": 1250,
"composability_score": 0.85,
"sample_summaries": [
"Redis cluster mode doesn't support pub/sub across shards",
"MULTI/EXEC transactions limited to single slot"
],
"composable": true
}
]
| Status | Meaning |
|---|---|
| 200 | Clusters returned |
| 403 | Insufficient role (requires analyst) |
| 409 | Clustering is disabled via configuration |
POST /api/v1/fragments/clusters/compose¶
Compose a composable cluster into a documentation draft. Checks for existing doc coverage before composing — if a document already covers the topic (>70% similarity), composition is skipped. Requires admin role.
Request Body:
{
"cluster": {
"topic": "Redis Cluster Mode Limitations",
"fragment_ids": ["uuid1", "uuid2", "uuid3", "uuid4", "uuid5"],
"source_diversity": 3,
"author_diversity": 4,
"total_content_words": 1250,
"composability_score": 0.85,
"sample_summaries": ["..."],
"composable": true
}
}
Response:
| Status | Meaning |
|---|---|
| 200 | Draft created successfully |
| 400 | Cluster not composable or invalid request |
| 403 | Insufficient role (requires admin) |
| 409 | Clustering disabled, or existing doc already covers this topic |
Space Ownership & Governance¶
Explicit knowledge ownership — spaces get owners, maintainers, and contributors. Topics get stewards who are auto-assigned when matching gaps are detected. This is the accountability layer that ensures gaps get resolved and drafts get reviewed.
Governance is configured via API, not environment variables.
GET /api/v1/governance/spaces¶
List all spaces with ownership summary (owner/maintainer/contributor counts). Requires viewer role.
Response:
{
"spaces": [
{ "space": "PLATFORM", "owner_count": 1, "maintainer_count": 2, "contributor_count": 5 },
{ "space": "INFRA", "owner_count": 0, "maintainer_count": 0, "contributor_count": 0 }
]
}
GET /api/v1/governance/spaces/:space/owners¶
List owners, maintainers, and contributors for a specific space. Requires viewer role.
Response:
{
"owners": [
{
"id": "uuid",
"space": "PLATFORM",
"user_id": "uuid",
"role": "owner",
"notifications_enabled": true,
"user_email": "alice@acme.com",
"user_display_name": "Alice"
}
]
}
POST /api/v1/governance/spaces/:space/owners¶
Add a user as owner, maintainer, or contributor of a space. Requires admin role.
Request body:
Valid roles: owner, maintainer, contributor.
Status codes: 201 Created, 409 if user already assigned, 400 if user not found.
DELETE /api/v1/governance/spaces/:space/owners/:user_id¶
Remove a user from a space's ownership. Requires admin role.
Status codes: 204 No Content, 404 if not found.
PATCH /api/v1/governance/spaces/:space/owners/:user_id¶
Update a space owner's role or notification preference. Requires admin role.
Request body:
At least one field must be provided.
GET /api/v1/governance/stewards¶
List all topic stewards with their regex patterns and auto-assign settings. Requires viewer role.
Response:
{
"stewards": [
{
"id": "uuid",
"topic_pattern": "kubernetes|k8s|eks",
"display_name": "Kubernetes Infrastructure",
"user_id": "uuid",
"auto_assign_gaps": true,
"auto_assign_fragments": true,
"user_email": "carol@acme.com",
"user_display_name": "Carol"
}
]
}
POST /api/v1/governance/stewards¶
Create a topic steward. The topic_pattern is a regex matched against gap labels and fragment content for auto-assignment. Requires admin role.
Request body:
{
"topic_pattern": "kubernetes|k8s|eks",
"display_name": "Kubernetes Infrastructure",
"user_id": "uuid",
"auto_assign_gaps": true,
"auto_assign_fragments": true
}
Pattern validation: max 500 characters, must be valid regex. Status codes: 201 Created, 400 invalid pattern or user not found.
GET /api/v1/governance/stewards/:id¶
Get a single topic steward by ID. Requires viewer role.
DELETE /api/v1/governance/stewards/:id¶
Remove a topic steward. Requires admin role. Returns 204 or 404.
PATCH /api/v1/governance/stewards/:id¶
Update a topic steward's pattern, display name, or auto-assign settings. Requires admin role.
Request body:
At least one field must be provided. New patterns are validated before saving.
GET /api/v1/governance/my-spaces¶
List spaces the current user owns or maintains. Requires an API key with an associated user_id.
GET /api/v1/governance/my-stewardships¶
List topics the current user stewards. Requires an API key with an associated user_id.
GET /api/v1/governance/coverage¶
Ownership coverage report across all spaces. Requires viewer role.
Response:
{
"total_spaces": 12,
"owned_spaces": 9,
"coverage_pct": 75.0,
"unowned_spaces": ["INFRA", "SECURITY", "ONBOARDING"]
}
Total spaces are derived from the documents table (distinct space values), not from governance tables — ensuring unowned spaces are visible.
GET /api/v1/governance/dashboard¶
Aggregated governance overview combining ownership coverage, SLA compliance, quality scores, fragment stats, review queue, velocity, and top contributors. Requires analyst role.
Each section is independently fetched with a 5-second timeout. If a section fails, it returns null and the failure is recorded in errors. The endpoint always returns 200 with partial data.
Response:
{
"coverage": {
"total_spaces": 12,
"owned_spaces": 9,
"coverage_pct": 75.0,
"unowned_spaces": ["INFRA"]
},
"sla_breaches": {
"total_open": 3,
"total_acknowledged": 12,
"by_type": [{ "sla_type": "gap_acknowledgment", "count": 2 }],
"by_space": [{ "space": "INFRA", "count": 2 }]
},
"quality": {
"overall_avg": 68.5,
"by_space": [{ "space": "PAYMENTS", "avg_score": 72.3, "doc_count": 15 }],
"worst_docs": [{ "document_id": "uuid", "title": "Old Guide", "composite_score": 23.4 }]
},
"fragments": {
"total": 147,
"by_status": [{ "status": "approved", "count": 89 }],
"by_source_type": [{ "source_type": "pr_merge", "count": 68 }],
"by_space": [{ "space": "PAYMENTS", "count": 45 }]
},
"review_queue": [
{
"draft_id": "uuid",
"draft_title": "Deployment Runbook",
"current_stage": "sme_review",
"stage_display_name": "SME Review",
"workflow_name": "Default",
"space": "INFRA",
"entered_stage_at": "2024-01-15T10:00:00Z"
}
],
"velocity": {
"current_velocity": 1.2,
"velocity_trend": "accelerating",
"grade": "B",
"weekly_snapshots": [
{ "week_start": "2024-01-08", "docs_created": 5, "docs_updated": 12, "gaps_opened": 3, "gaps_resolved": 4, "velocity": 1.1 }
]
},
"top_contributors": [
{ "author_id": "alice", "author_name": "Alice Smith", "fragment_count": 23, "approved_count": 18 }
],
"errors": [],
"generated_at": "2024-01-15T12:00:00Z"
}
Sections that fail to load return null with an entry in errors:
Governance SLAs¶
SLA policies define maximum acceptable times for gap acknowledgment, gap resolution, draft review, and document freshness. Policies can be set per-space or org-wide (default). A periodic background checker detects breaches and emits SlaBreached events.
GET /api/v1/governance/slas¶
List all SLA policies. Requires viewer role.
Response:
{
"policies": [
{
"id": "uuid",
"space": null,
"gap_acknowledgment_hours": 48,
"gap_resolution_days": 14,
"draft_review_hours": 72,
"freshness_review_days": 30,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}
The space: null row is the org-wide default (always present). Space-specific overrides appear with their space name.
PUT /api/v1/governance/slas/default¶
Upsert the org-wide default SLA policy. Requires admin role.
Request body (all fields optional — omitted fields keep current values):
{
"gap_acknowledgment_hours": 48,
"gap_resolution_days": 14,
"draft_review_hours": 72,
"freshness_review_days": 30
}
Validation: Hours must be 1–8760, days must be 1–365.
PUT /api/v1/governance/slas/:space¶
Upsert a space-specific SLA policy override. Requires admin role. Same request body as above.
DELETE /api/v1/governance/slas/:space¶
Delete a space-specific override (space falls back to org default). Requires admin role. Returns 204 No Content on success, 404 if no override exists for the space.
GET /api/v1/governance/breaches¶
List SLA breaches with optional filters. Requires viewer role.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
space |
string | — | Filter by space |
sla_type |
string | — | Filter: acknowledgment, resolution, review, freshness |
open_only |
bool | false |
Only show unacknowledged breaches |
limit |
int | 50 |
Max results (up to 200) |
offset |
int | 0 |
Pagination offset |
Response:
{
"breaches": [
{
"id": "uuid",
"entity_type": "gap",
"entity_id": "uuid",
"sla_type": "acknowledgment",
"space": "ENGINEERING",
"owner_id": null,
"hours_overdue": 12.5,
"acknowledged_at": null,
"acknowledged_by": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}
GET /api/v1/governance/breaches/summary¶
Aggregate breach statistics for dashboards. Requires viewer role.
Response:
{
"total_open": 5,
"total_acknowledged": 12,
"by_type": [
{ "sla_type": "acknowledgment", "count": 3 },
{ "sla_type": "freshness", "count": 2 }
],
"by_space": [
{ "space": "ENGINEERING", "count": 4 },
{ "space": "(org-wide)", "count": 1 }
]
}
POST /api/v1/governance/breaches/:id/acknowledge¶
Mark a breach as acknowledged. Requires editor role. The acknowledging user is recorded. Returns { "acknowledged": true } on success, 404 if the breach doesn't exist or was already acknowledged.
Review Workflows¶
Configurable multi-stage review pipelines for autopilot drafts. Each space can have a workflow that defines approval stages (e.g., SME Review → Writer Review → Publish Approval). Reviewers approve, request changes, or reject drafts at each stage.
GET /api/v1/governance/workflows¶
List all review workflows. Requires viewer role.
Response:
{
"workflows": [
{
"id": "uuid",
"space": "ENGINEERING",
"name": "Standard Review",
"stages": [
{ "name": "sme_review", "required_role": "maintainer", "approvals_needed": 1 },
{ "name": "writer_review", "required_role": "contributor", "approvals_needed": 1 }
],
"is_default": false,
"created_at": "2026-03-22T10:00:00Z",
"updated_at": "2026-03-22T10:00:00Z"
}
]
}
POST /api/v1/governance/workflows¶
Create a review workflow for a space. Each space can have at most one workflow. Requires admin role.
Request body:
{
"space": "ENGINEERING",
"name": "Standard Review",
"stages": [
{ "name": "sme_review", "required_role": "maintainer", "approvals_needed": 1 },
{ "name": "writer_review", "required_role": "contributor", "approvals_needed": 1 }
],
"is_default": false
}
Validation rules:
- Maximum 10 stages per workflow
- Stage names must be unique within a workflow
- required_role must be one of: owner, maintainer, contributor
- approvals_needed must be between 1 and 20
PATCH /api/v1/governance/workflows/:id¶
Update a workflow's name, stages, or default flag. Requires admin role.
DELETE /api/v1/governance/workflows/:id¶
Delete a workflow. Drafts assigned to this workflow retain their current stage but lose the workflow reference (ON DELETE SET NULL). Requires admin role.
POST /api/v1/drafts/:id/review¶
Submit a review action (approve, request changes, or reject) for a draft at its current stage. Requires editor role. The reviewer must also hold the stage's required_role in the draft's space governance.
Request body:
Valid actions: approve, request_changes, reject.
- approve: Counts toward the stage's
approvals_neededthreshold. When the threshold is met, the draft advances to the next stage (or becomesreviewedif it was the final stage). - request_changes: Recorded but does not block approvals. The draft stays at the current stage.
- reject: Immediately sets the draft status to
rejected.
Each reviewer can submit one action per stage. Submitting again updates the existing action (upsert).
Response: 200 OK with {"ok": true, "advanced": true} if the draft advanced to the next stage.
POST /api/v1/drafts/:id/skip-review¶
Admin bypass: skip all remaining review stages and move the draft directly to reviewed status. Requires admin role.
Use this when a draft doesn't need gatekeeping — e.g., trusted content sources, low-risk updates, or time-sensitive documentation.
Response: 200 OK with {"ok": true, "status": "reviewed"}
Returns 404 if the draft is not found or not in a reviewable state (must be status = 'draft').
GET /api/v1/drafts/:id/reviews¶
List all review actions for a draft. Requires viewer role.
GET /api/v1/drafts/:id/stage¶
Get the current review stage progress for a draft, including who has reviewed and how many approvals remain.
Response:
{
"draft_id": "uuid",
"current_stage": "sme_review",
"workflow_id": "uuid",
"progress": {
"stage_name": "sme_review",
"approvals_needed": 2,
"approvals_received": 1,
"actions": [
{
"actor_id": "uuid",
"action": "approve",
"note": "LGTM",
"created_at": "2026-03-22T11:00:00Z"
}
]
}
}
POST /api/v1/drafts/:id/comments¶
Add a review comment to a draft. Requires editor role.
Request body:
Comment body is limited to 10,000 characters. parent_id enables threaded replies.
GET /api/v1/drafts/:id/comments¶
List all comments on a draft, ordered by creation time. Requires viewer role.
PATCH /api/v1/comments/:id¶
Update a comment's body. Only the comment author or an admin can edit. Requires editor role.
POST /api/v1/comments/:id/resolve¶
Mark a comment as resolved. Requires editor role.
GET /api/v1/reviews/my-queue¶
List drafts awaiting the current user's review, based on their space governance roles. Returns drafts where the user holds the required role for the current stage.
Response:
{
"items": [
{
"draft_id": "uuid",
"title": "Getting Started with Kubernetes",
"space": "ENGINEERING",
"current_stage": "sme_review",
"workflow_name": "Standard Review",
"created_at": "2026-03-22T09:00:00Z"
}
]
}
Content Quality Scoring¶
Deterministic structural quality scores for documents and fragments. Each item receives a composite score (0-100) built from 7 sub-scores, with content-type-aware templates defining completeness expectations.
List Quality Scores¶
Paginated list of quality scores with optional filters. Requires viewer role.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
space |
string | — | Filter by space |
min_score |
float | — | Minimum composite score (0-100) |
max_score |
float | — | Maximum composite score (0-100) |
content_type |
string | — | Filter by content type |
limit |
integer | 50 | Max results (capped at 200) |
offset |
integer | 0 | Pagination offset |
Response:
{
"scores": [
{
"id": "uuid",
"document_id": "uuid",
"fragment_id": null,
"heading_structure": 15.0,
"section_completeness": 20.0,
"code_presence": 10.0,
"link_density": 7.0,
"content_length": 8.0,
"readability": 12.0,
"metadata_quality": 10.0,
"composite_score": 82.0,
"scored_at": "2025-01-15T10:30:00Z"
}
]
}
Get Document Score¶
Quality score for a specific document. Requires viewer role.
Response:
{
"score": {
"id": "uuid",
"document_id": "uuid",
"heading_structure": 15.0,
"section_completeness": 20.0,
"code_presence": 10.0,
"link_density": 7.0,
"content_length": 8.0,
"readability": 12.0,
"metadata_quality": 10.0,
"composite_score": 82.0,
"scored_at": "2025-01-15T10:30:00Z"
},
"status": "high"
}
Status values: high (80+), acceptable (60+), needs_improvement (40+), poor (<40).
Trigger Rescore¶
Triggers a rescore of all documents. Returns immediately — rescoring happens asynchronously during the next ingest cycle. Requires admin role.
Response:
{
"status": "accepted",
"documents_to_score": 1234,
"message": "Rescoring will happen during the next ingest cycle"
}
Quality Report¶
Aggregate quality report with per-space breakdown and worst-scoring documents. Requires analyst role.
Response:
{
"overall_avg": 72.5,
"total_scored": 1234,
"by_space": [
{
"space": "ENGINEERING",
"avg_score": 78.3,
"document_count": 450,
"worst_docs": [
{
"document_id": "uuid",
"title": "Legacy Migration Guide",
"composite_score": 23.5
}
]
}
]
}
List Content Type Templates¶
Returns the built-in content type templates that define section completeness expectations. Requires viewer role.
Response:
{
"templates": [
{
"content_type": "runbook",
"required_sections": ["overview", "prerequisites", "steps", "rollback", "escalation"],
"optional_sections": ["monitoring", "troubleshooting"],
"min_word_count": 200,
"max_word_count": 5000,
"expect_code_blocks": true
},
{
"content_type": "guide",
"required_sections": ["introduction", "prerequisites", "steps"],
"optional_sections": ["examples", "faq", "next steps"],
"min_word_count": 300,
"max_word_count": 10000,
"expect_code_blocks": true
}
]
}
Available content types: runbook, guide, troubleshooting, faq, reference.
Sub-Score Breakdown¶
| Sub-Score | Range | What It Measures |
|---|---|---|
heading_structure |
0-20 | Presence of headings, proper hierarchy (H1→H2→H3), no skipped levels |
section_completeness |
0-25 | Required sections present per content type template |
code_presence |
0-10 | Code blocks present when expected by content type |
link_density |
0-10 | Internal/external links for cross-referencing |
content_length |
0-10 | Word count within template-defined min/max range |
readability |
0-15 | Sentence length variation, no wall-of-text paragraphs, manageable sentence lengths |
metadata_quality |
0-10 | Author, source URL, and space metadata present |
Semantic Quality (LLM-Assessed)¶
When semantic quality scoring is enabled, documents are also assessed by an LLM on four dimensions:
| Dimension | Range | Description |
|---|---|---|
accuracy |
0-25 | Claims grounded in cited sources |
completeness |
0-25 | Covers all aspects a reader needs |
clarity |
0-25 | Understandable without external help |
actionability |
0-25 | Provides concrete steps, commands, and examples |
The semantic score (0-100) is stored in the semantic_score field with per-dimension details in semantic_details. The composite_score becomes a 50/50 blend of structural and semantic scores once both are available.
Semantic scoring runs as a background sweep (configurable interval, default 24h) and only evaluates documents with structural_total >= 40 to avoid wasting LLM calls on obviously poor content. Newly generated drafts are scored immediately after creation.
Style Rules Engine¶
Configurable linting rules for documentation consistency. Rules are scoped globally or per-space, with space-specific rules overriding global rules of the same type and name.
List Style Rules¶
Requires viewer role.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
space |
string | — | Filter by space |
rule_type |
string | — | Filter by type: terminology, formatting, structure, custom_pattern |
include_inactive |
boolean | false |
Include inactive rules (admin only) |
Response:
{
"rules": [
{
"id": "uuid",
"space": null,
"rule_type": "terminology",
"name": "avoid-simple",
"description": "Avoid the word 'simple' — it dismisses reader difficulty",
"config": { "term": "simple", "suggestion": "straightforward" },
"severity": "warning",
"is_active": true,
"created_at": "2026-03-22T00:00:00Z",
"updated_at": "2026-03-22T00:00:00Z"
}
]
}
Create Style Rule¶
Requires admin role.
Request Body:
{
"space": null,
"rule_type": "terminology",
"name": "avoid-simple",
"description": "Avoid the word 'simple'",
"config": { "term": "simple", "suggestion": "straightforward" },
"severity": "warning"
}
| Field | Type | Required | Description |
|---|---|---|---|
space |
string | no | Space scope (null = global) |
rule_type |
string | yes | terminology, formatting, structure, custom_pattern |
name |
string | yes | Unique name (1-200 chars) |
description |
string | no | Human-readable description (max 2000 chars) |
config |
object | yes | Rule-type-specific configuration (see below) |
severity |
string | no | error, warning, info (default: warning) |
Config by rule type:
| Rule Type | Config Schema |
|---|---|
terminology |
{ "term": "string", "suggestion": "string" } |
formatting |
{ "max_heading_depth": number } or { "max_sentence_length": number } |
structure |
{ "require_intro": true } |
custom_pattern |
{ "pattern": "regex", "message": "string" } |
Response: 201 Created
Status codes: 400 invalid input, 409 duplicate (space + type + name), 422 rule limit reached.
Update Style Rule¶
Requires admin role. Only provided fields are updated.
Request Body:
{
"description": "Updated description",
"config": { "term": "simple", "suggestion": "clear" },
"severity": "error",
"is_active": false
}
Delete Style Rule¶
Requires admin role. Returns 204 No Content or 404.
Import Rules from YAML¶
Requires admin role. Upserts rules from a YAML string (max 100 rules per import). Existing rules with the same (space, type, name) are updated.
Request Body:
{
"yaml": "- rule_type: terminology\n name: avoid-simple\n config:\n term: simple\n suggestion: straightforward\n severity: warning\n"
}
Response:
Export Rules to YAML¶
Requires admin role. Returns all rules as a YAML document (Content-Type: application/x-yaml).
Lint Content¶
Requires analyst role. Runs all active rules against the provided content.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
content |
string | yes | Text to lint (max 500KB) |
space |
string | no | Space for rule scoping (global + space rules apply) |
Response:
{
"violations": [
{
"rule_name": "avoid-simple",
"rule_type": "terminology",
"severity": "warning",
"message": "Avoid 'simple' — consider 'straightforward' instead",
"line": 1,
"column": 11,
"span": "simple"
}
],
"style_score": 95.0,
"summary": {
"errors": 0,
"warnings": 1,
"infos": 0,
"total": 1
},
"truncated": false
}
Style score formula: max(0, 100 - (errors × 15 + warnings × 5 + infos × 1)), clamped to [0, 100].
Limits¶
| Limit | Value |
|---|---|
| Max rules per space | 200 |
| Max total rules | 1000 |
| Max import batch | 100 |
| Max lint content | 500 KB |
| Max violations per lint | 100 |
| Max regex pattern length | 500 chars |
Webhooks¶
Outbound webhook subscriptions for pushing DocBrain events to external systems. All endpoints require admin role.
DocBrain signs every delivery with HMAC-SHA256 (X-DocBrain-Signature header), retries failed deliveries with exponential backoff, and automatically disables subscriptions after repeated failures (circuit breaker).
List Webhook Subscriptions¶
Response:
[
{
"id": "uuid",
"name": "Slack Pipeline",
"url": "https://hooks.example.com/docbrain",
"events": ["document.ingested", "gap.detected"],
"headers": { "X-Custom": "value" },
"is_active": true,
"created_by": "uuid",
"failure_count": 0,
"last_failure_at": null,
"last_success_at": "2026-03-22T10:00:00Z",
"disabled_reason": null,
"created_at": "2026-03-20T08:00:00Z",
"updated_at": "2026-03-22T10:00:00Z"
}
]
Note: The
secretfield is never returned in API responses.
Create Webhook Subscription¶
Request Body:
{
"name": "Slack Pipeline",
"url": "https://hooks.example.com/docbrain",
"secret": "your-secret-at-least-16-chars",
"events": ["document.ingested", "gap.detected"],
"headers": { "X-Custom": "value" }
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Human-readable subscription name |
url |
string | yes | HTTPS endpoint to deliver events to. Must not be a private/internal IP unless ALLOW_INTERNAL_WEBHOOKS=true. |
secret |
string | yes | HMAC signing secret (minimum 16 characters) |
events |
string[] | yes | Event types to subscribe to (see list below) |
headers |
object | no | Extra HTTP headers to include in deliveries |
Response: 201 Created with the WebhookSubscription object.
Validation:
- secret must be at least 16 characters
- events must contain only valid event types
- url must not resolve to a private/internal IP address (unless ALLOW_INTERNAL_WEBHOOKS=true)
Update Webhook Subscription¶
Request Body: (all fields optional)
{
"name": "Updated Name",
"url": "https://new-endpoint.example.com/hook",
"secret": "new-secret-at-least-16-chars",
"events": ["document.ingested"],
"headers": { "X-Custom": "new-value" },
"is_active": true
}
Response: 200 OK with the updated WebhookSubscription object.
Delete Webhook Subscription¶
Response:
Send Test Event¶
Sends a webhook.test event to the subscription's URL. Useful for verifying connectivity and signature validation.
Response:
List Delivery Log¶
Returns recent delivery attempts for a subscription.
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
limit |
integer | 50 |
Max results |
Response: Array of WebhookDelivery objects with delivery ID, event type, HTTP status, response body, attempt number, and timestamps.
Reset Circuit Breaker¶
Resets the failure counter and re-enables a subscription that was auto-disabled by the circuit breaker.
Response:
Event Types¶
All event types from the internal event bus are available for webhook subscriptions:
| Event Type | Description |
|---|---|
document.ingested |
New document indexed |
document.updated |
Existing document re-indexed |
document.deleted |
Document removed |
freshness.changed |
Document freshness status changed |
quality.scored |
Quality score computed or updated |
fragment.captured |
Knowledge fragment captured |
fragment.indexed |
Fragment auto-indexed into search |
fragment.promoted |
Fragment promoted from review queue |
gap.detected |
New gap cluster detected |
gap.assigned |
Gap assigned to a user |
gap.resolved |
Gap marked as resolved |
draft.generated |
AI draft generated for a gap |
draft.review_requested |
Draft sent for review |
draft.published |
Draft published to target system |
draft.rejected |
Draft rejected during review |
query.answered |
User query answered |
feedback.received |
User feedback submitted |
sla.breached |
Documentation SLA breached |
maintenance.fix_proposed |
Automated fix proposal generated |
Delivery Headers¶
Every webhook delivery includes these headers:
| Header | Description |
|---|---|
Content-Type |
application/json |
X-DocBrain-Signature |
HMAC-SHA256 signature: sha256=<hex-digest>. Compute HMAC-SHA256(secret, raw_body) and compare to verify authenticity. |
X-DocBrain-Event |
Event type (e.g. document.ingested) |
X-DocBrain-Delivery |
Unique delivery UUID for idempotency |
Retry Policy¶
Failed deliveries (non-2xx response or timeout) are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 60 seconds |
| 3 | 300 seconds (5 minutes) |
| 4 | 3600 seconds (1 hour) |
After all retry attempts are exhausted, the failure counter is incremented. When the failure counter reaches WEBHOOK_CIRCUIT_BREAKER_THRESHOLD (default: 10) consecutive failures, the subscription is automatically disabled with disabled_reason: "circuit_breaker". Use POST /api/v1/webhooks/:id/reset to re-enable.
External Connectors¶
External connectors are stateless HTTP servers that implement a simple REST contract. DocBrain calls them on a cron schedule to ingest documents. All management endpoints require admin role. Status endpoint requires viewer+ role.
List Connectors¶
Response:
[
{
"id": "uuid",
"name": "internal-wiki",
"display_name": "Internal Wiki",
"base_url": "https://wiki-connector.example.com",
"source_type": "wiki",
"schedule_cron": "0 */6 * * *",
"space": "engineering",
"is_active": true,
"last_sync_at": "2026-03-24T06:00:00Z",
"last_sync_docs": 42,
"last_error": null,
"consecutive_failures": 0,
"created_at": "2026-03-20T08:00:00Z",
"updated_at": "2026-03-24T06:00:00Z"
}
]
Note: The
auth_headerfield is never returned in API responses.
Create Connector¶
Request Body:
{
"name": "internal-wiki",
"display_name": "Internal Wiki",
"base_url": "https://wiki-connector.example.com",
"auth_header": "Bearer your-token",
"source_type": "wiki",
"schedule_cron": "0 */6 * * *",
"space": "engineering"
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Unique machine name (1-100 chars) |
display_name |
string | yes | Human-readable name (1-100 chars) |
base_url |
string | yes | Connector HTTP endpoint (1-2048 chars, must start with http:// or https://) |
auth_header |
string | no | Authorization header value (e.g. Bearer token) |
source_type |
string | yes | Document source type (1-20 chars, must be unique across connectors) |
schedule_cron |
string | no | Standard 5-field cron expression (default: 0 */6 * * * = every 6 hours) |
space |
string | no | Target space for ingested documents |
Response: 201 Created with the connector object.
Validation:
- base_url must not resolve to a private/internal IP address (unless CONNECTOR_ALLOW_INTERNAL=true)
- schedule_cron must be a valid cron expression
- source_type must be unique — returns 409 Conflict if already registered
Update Connector¶
Request Body: (all fields optional)
{
"display_name": "Updated Wiki",
"base_url": "https://new-url.example.com",
"auth_header": "Bearer new-token",
"source_type": "wiki",
"schedule_cron": "0 */12 * * *",
"space": "docs",
"is_active": true
}
Response: 200 OK with the updated connector object. Returns 404 if not found.
Delete Connector¶
Response: 200 OK with {"deleted": true}. Returns 404 if not found.
Trigger Manual Sync¶
Triggers an immediate sync for the connector, bypassing the cron schedule and circuit breaker. Returns an error if a sync is already in progress for this connector.
Response:
| Status | Meaning |
|---|---|
| 200 | Sync completed successfully |
| 404 | Connector not found |
| 409 | Sync already in progress |
Connector Status¶
Returns sync health information for a connector. Requires viewer+ role.
Response:
{
"id": "uuid",
"name": "internal-wiki",
"is_active": true,
"last_sync_at": "2026-03-24T06:00:00Z",
"last_sync_docs": 42,
"last_error": null,
"consecutive_failures": 0
}
Test Connector Health¶
Runs a health check against the connector's /health endpoint without triggering a sync.
Response:
If the health check fails:
Connector Protocol¶
External connectors must implement three endpoints:
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Returns {"status": "ok"} and optionally {"version": "..."} |
/documents/list |
POST | Lists available documents (paginated, supports incremental sync via since) |
/documents/fetch |
POST | Returns full document content for a list of source IDs |
List Request:
List Response:
{
"documents": [
{ "source_id": "doc-123", "title": "Getting Started", "updated_at": "2026-03-24T10:00:00Z" }
],
"has_more": false,
"total": 1
}
Fetch Request:
Fetch Response:
{
"documents": [
{
"source_id": "doc-123",
"title": "Getting Started",
"content": "# Getting Started\n\nWelcome to...",
"content_type": "markdown",
"url": "https://wiki.example.com/getting-started",
"author": "Jane Doe",
"updated_at": "2026-03-24T10:00:00Z",
"metadata": {},
"references": []
}
]
}
CI/CD Pipeline Capture¶
Automated knowledge extraction from merged PRs and deployments. Requires editor role.
POST /api/v1/ci/analyze¶
Analyze a merged PR and extract knowledge fragments.
Request body:
{
"pr_number": 1234,
"repo": "acme/platform",
"pr_title": "Switch event delivery to PG LISTEN/NOTIFY",
"pr_body": "Replaced Redis pub/sub with PostgreSQL...",
"diff_stat": "+120 -45",
"changed_files": "src/events/publisher.rs,src/events/subscriber.rs",
"labels": "architecture,breaking-change",
"author": "alice@acme.com"
}
| Field | Type | Required | Description |
|---|---|---|---|
pr_number |
integer | yes | Pull request number |
repo |
string | yes | Repository in owner/name format |
pr_title |
string | yes | PR title (max 500 chars) |
pr_body |
string | no | PR description (max 50,000 chars) |
diff_stat |
string | no | Git diff stats |
changed_files |
string | no | Comma-separated list of changed files |
labels |
string | no | Comma-separated PR labels |
author |
string | no | PR author email or username |
Response:
{
"fragments_created": 2,
"fragments": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"fragment_type": "decision",
"summary": "Switched event delivery from Redis to PG LISTEN/NOTIFY",
"confidence": 0.85,
"routed_action": "auto_index"
}
],
"already_analyzed": false
}
| Status | Description |
|---|---|
| 200 | Analysis complete (check fragments_created and already_analyzed) |
| 400 | Validation error (missing required field, field too long) |
| 403 | Insufficient role (requires editor+) |
| 503 | CI analysis is disabled (CI_ANALYZE_ENABLED=false) |
The endpoint is idempotent: re-analyzing the same PR returns already_analyzed: true with no new fragments.
POST /api/v1/ci/deploy-capture¶
Capture deployment context as a knowledge fragment.
Request body:
{
"service": "payment-gateway",
"version": "2.4.1",
"environment": "production",
"changelog": "abc1234 Fixed retry logic\ndef5678 Updated timeout config",
"config_diff": "timeout: 30 -> 60"
}
| Field | Type | Required | Description |
|---|---|---|---|
service |
string | yes | Service name (max 256 chars) |
version |
string | yes | Version being deployed (max 128 chars) |
environment |
string | yes | Target environment (max 128 chars) |
changelog |
string | no | Git changelog (max 50,000 chars) |
config_diff |
string | no | Configuration changes (max 50,000 chars) |
Response:
{
"fragment_id": "550e8400-e29b-41d4-a716-446655440000",
"summary": "Deployed payment-gateway v2.4.1 to production with retry logic fix"
}
| Status | Description |
|---|---|
| 200 | Capture complete |
| 400 | Validation error |
| 403 | Insufficient role |
| 503 | CI analysis is disabled |
GitHub Action Setup¶
Add this workflow to your repository to automatically capture knowledge from merged PRs:
name: DocBrain Knowledge Capture
on:
pull_request:
types: [closed]
jobs:
capture:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Capture knowledge from PR
env:
DOCBRAIN_API_URL: ${{ secrets.DOCBRAIN_API_URL }}
DOCBRAIN_API_KEY: ${{ secrets.DOCBRAIN_API_KEY }}
run: |
curl -s -X POST "${DOCBRAIN_API_URL}/api/v1/ci/analyze" \
-H "Authorization: Bearer ${DOCBRAIN_API_KEY}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--argjson pr_number ${{ github.event.pull_request.number }} \
--arg repo '${{ github.repository }}' \
--arg pr_title '${{ github.event.pull_request.title }}' \
'{pr_number: $pr_number, repo: $repo, pr_title: $pr_title}')"
Required repository secrets:
- DOCBRAIN_API_URL — Your DocBrain server URL
- DOCBRAIN_API_KEY — API key with editor+ role