Engram — API Design
Base URL: /api/v1
All responses are JSON. All request bodies are JSON. Authentication via Authorization: Bearer <token> (JWT or API key).
Authentication
Register
POST /auth/register
Body: { email, password, name }
Response: 201 { user, accessToken, refreshToken }
Login
POST /auth/login
Body: { email, password }
Response: 200 { user, accessToken, refreshToken }
Refresh Token
POST /auth/refresh
Body: { refreshToken }
Response: 200 { accessToken, refreshToken }
Refresh tokens are single-use (rotation). Old token invalidated on use.
OAuth2 Initiate
GET /auth/oauth/{provider}/authorize?redirect_uri=...
Response: 302 redirect to provider (GitHub, Google)
OAuth2 Callback
GET /auth/oauth/{provider}/callback?code=...&state=...
Response: 302 redirect to app with tokens in query params
Get Current User
GET /auth/me
Response: 200 { id, email, name, avatarUrl }
API Keys
POST /workspaces/{wid}/api-keys
Body: { name }
Response: 201 { id, name, key, keyPrefix }
Note: `key` is returned ONCE. Only keyPrefix stored for display.
GET /workspaces/{wid}/api-keys
Response: 200 [{ id, name, keyPrefix, lastUsedAt, createdAt }]
DELETE /workspaces/{wid}/api-keys/{id}
Response: 204
Workspaces
Create Workspace
POST /workspaces
Body: { name, slug }
Response: 201 { workspace }
Note: Creator automatically added as owner.
List My Workspaces
GET /workspaces
Response: 200 [{ workspace, role }]
Get Workspace
GET /workspaces/{wid}
Response: 200 { workspace, memberCount, domainCount, documentCount }
Update Workspace
PATCH /workspaces/{wid}
Body: { name?, slug?, settings? }
Response: 200 { workspace }
Auth: owner or admin
Delete Workspace
DELETE /workspaces/{wid}
Response: 204
Auth: owner only
Members
GET /workspaces/{wid}/members
Response: 200 [{ userId, email, name, role, joinedAt }]
POST /workspaces/{wid}/members
Body: { email, role }
Response: 201 { member }
Auth: owner or admin
PATCH /workspaces/{wid}/members/{userId}
Body: { role }
Response: 200 { member }
Auth: owner or admin
DELETE /workspaces/{wid}/members/{userId}
Response: 204
Auth: owner or admin (cannot remove last owner)
Domains
All domain endpoints are workspace-scoped: /workspaces/{wid}/domains
CRUD
POST /workspaces/{wid}/domains
Body: { name, slug, category, description?, icon? }
Response: 201 { domain }
Auth: editor+
GET /workspaces/{wid}/domains
Response: 200 [{ domain, documentCount, ownerCount }]
GET /workspaces/{wid}/domains/{did}
Response: 200 { domain, documents: [...], owners: [...] }
PATCH /workspaces/{wid}/domains/{did}
Body: { name?, description?, category?, icon? }
Response: 200 { domain }
Auth: editor+
DELETE /workspaces/{wid}/domains/{did}
Response: 204
Auth: admin+
Owners
PUT /workspaces/{wid}/domains/{did}/owners
Body: { userIds: [...] }
Response: 200 { owners: [...] }
Auth: admin+
Documents
All document endpoints are workspace-scoped: /workspaces/{wid}/documents
CRUD
POST /workspaces/{wid}/documents
Body: { domainId, title, slug, content?, status?, metadata? }
Response: 201 { document }
Auth: editor+
GET /workspaces/{wid}/documents
Query: ?domainId=...&status=...&search=...
Response: 200 { documents: [...], total }
GET /workspaces/{wid}/documents/{docId}
Response: 200 { document, recentVersions: [...] }
PATCH /workspaces/{wid}/documents/{docId}
Body: { title?, content?, status?, metadata? }
Response: 200 { document }
Note: Auto-increments version. Previous content saved to document_versions.
Auth: editor+
DELETE /workspaces/{wid}/documents/{docId}
Response: 204
Auth: admin+
Versions
GET /workspaces/{wid}/documents/{docId}/versions
Query: ?limit=20&offset=0
Response: 200 [{ id, version, createdAt, createdBy }]
GET /workspaces/{wid}/documents/{docId}/versions/{version}
Response: 200 { id, version, content, createdAt, createdBy }
POST /workspaces/{wid}/documents/{docId}/restore/{version}
Response: 200 { document }
Note: Creates a new version with the restored content (append-only).
Auth: editor+
Generate Document (AI)
POST /workspaces/{wid}/documents/generate
Body: { topicTitle, topicCategory, domainId, sourceRefs?: [...] }
Response: 202 { jobId }
Note: Async job. Poll via SSE.
Auth: editor+
Sources
Connections
POST /workspaces/{wid}/sources
Body: { type, credentials?, config? }
Response: 201 { sourceConnection }
Auth: admin+
GET /workspaces/{wid}/sources
Response: 200 [{ sourceConnection, mappingCount, lastSyncedAt }]
GET /workspaces/{wid}/sources/{sid}
Response: 200 { sourceConnection, mappings: [...] }
PATCH /workspaces/{wid}/sources/{sid}
Body: { config? }
Response: 200 { sourceConnection }
Auth: admin+
DELETE /workspaces/{wid}/sources/{sid}
Response: 204
Auth: admin+
Sync
POST /workspaces/{wid}/sources/{sid}/sync
Response: 202 { jobId }
Note: Async job. Fetches pages from source, creates/updates mappings.
Auth: admin+
Gap Analysis
Gap analysis is source-first — no manual company profile form. It ingests all connected sources (GitHub repos, Notion, Confluence, website, Google Workspace) and produces three buckets.
POST /workspaces/{wid}/gap-analysis
Body: { sourceIds?: [...] }
Response: 202 { jobId }
Note: Async job. Analyzes all synced sources (or subset if sourceIds provided).
Produces three buckets: canCreate, issuesFound, missingCritical.
Requires at least one source to be connected and synced.
Auth: admin+
GET /workspaces/{wid}/gap-analysis
Response: 200 {
gapAnalysis: {
id, workspaceId, coverageScore,
companyUnderstanding: {
industry, productType, techStack, teamSize,
summary
},
canCreate: [{
title, suggestedDomain, confidence,
sourceEvidence: [{ sourceRef, excerpt, type }],
implicit: boolean
}],
issuesFound: [{
severity: "error" | "warning",
type: "contradiction" | "divergence" | "quality" | "staleness",
message,
locations: [{ sourceType, sourceRef, excerpt }],
suggestion
}],
missingCritical: [{
title, suggestedDomain, reason,
canGenerateStarter: boolean
}],
createdAt, updatedAt
}
}
Note: Returns latest analysis.
POST /workspaces/{wid}/gap-analysis/confirm
Body: { topics: [{ title, domain }] }
Response: 200 { gapAnalysis }
Note: Moves topics from canCreate/missingCritical to confirmed. Ready for document generation.
Auth: editor+
POST /workspaces/{wid}/gap-analysis/dismiss
Body: { topics: [{ title, reason? }] }
Response: 200 { gapAnalysis }
Auth: editor+
Validation
POST /workspaces/{wid}/validate
Body: { checks?: ["quality", "consistency", "compliance"] }
Response: 202 { jobId }
Note: Runs selected checks (default: all). Async job.
Auth: editor+
GET /workspaces/{wid}/validation/latest
Response: 200 {
quality: { scores: [{ documentId, overall, specificity, actionability, completeness, formatting }] },
consistency: { findings: [{ severity, message, documentSlugs, suggestion }] },
compliance: { violations: [{ severity, message, filePath, suggestion }] }
}
Compilation
Compile (preview)
POST /workspaces/{wid}/compile
Body: { targets?: ["claude", "cursor", "copilot", "windsurf", "agents-md"] }
Response: 200 {
outputs: {
claude: [{ path: ".claude/docs/engram/api-conventions.md", content: "..." }, ...],
cursor: [{ path: ".cursor/rules/engram-api-conventions.mdc", content: "..." }, ...],
...
}
}
Note: Synchronous. Returns compiled file outputs without deploying.
Deploy to Repos
POST /workspaces/{wid}/deploy
Body: { repoIds?: [...], targets?: [...] }
Response: 202 { jobId }
Note: Async job. Compiles + pushes to connected repos via GitHub App.
Auth: admin+
Repos
POST /workspaces/{wid}/repos
Body: { url, provider?, branch?, compileTargets, autoMerge? }
Response: 201 { repo }
Auth: admin+
GET /workspaces/{wid}/repos
Response: 200 [{ repo, lastCompiledAt }]
PATCH /workspaces/{wid}/repos/{rid}
Body: { branch?, compileTargets?, autoMerge? }
Response: 200 { repo }
Auth: admin+
DELETE /workspaces/{wid}/repos/{rid}
Response: 204
Auth: admin+
Context Serving (Hot Path)
The primary endpoint for CLI and AI agents. Optimized for speed.
GET /context
Query params:
workspace=<slug> (required)
role=<role> (optional — filter by role)
category=<category> (optional — filter by category)
file=<path> (optional — file-aware context)
format=markdown|json (optional — default: markdown)
Response: 200
markdown format: plain markdown text (text/markdown content-type)
json format: { documents: [{ title, slug, domain, content }] }
Auth: API key (Bearer token)
Cache: Served from compiled_outputs table or Redis cache. <10ms target.
Semantic Search
GET /context/search
Query params:
workspace=<slug> (required)
q=<query> (required)
limit=<n> (optional — default: 5)
Response: 200 {
results: [{ documentId, title, slug, excerpt, score }]
}
Auth: API key
Routing Config
GET /workspaces/{wid}/routing
Response: 200 { routingConfig }
PUT /workspaces/{wid}/routing
Body: { roles, roleMappings, readingOrders, alwaysInclude }
Response: 200 { routingConfig }
Auth: admin+
Jobs (SSE)
Get Job Status
GET /workspaces/{wid}/jobs/{jobId}
Response: 200 { job }
Stream Job Events
GET /workspaces/{wid}/jobs/{jobId}/events
Response: 200 (text/event-stream)
Events:
data: { "status": "queued" }
data: { "status": "running" }
data: { "status": "running", "progress": 30 }
data: { "status": "running", "progress": 70 }
data: { "status": "completed", "result": { ... } }
-- or --
data: { "status": "failed", "error": "..." }
Webhooks (Incoming)
GitHub App Webhook
POST /webhooks/github
Headers: X-GitHub-Event, X-Hub-Signature-256
Body: GitHub webhook payload
Handled events:
- installation: App installed/uninstalled on a repo
- push: Detect convention drift (compare code changes against guidelines)
- pull_request: Same drift detection, at PR level
Error Format
All errors follow a consistent structure:
{
"error": {
"code": "WORKSPACE_NOT_FOUND",
"message": "Workspace with slug 'acme' not found",
"status": 404
}
}
Common error codes:
UNAUTHORIZED(401) — missing or invalid tokenFORBIDDEN(403) — insufficient roleNOT_FOUND(404) — resource doesn't existCONFLICT(409) — slug already taken, duplicate memberVALIDATION_ERROR(422) — invalid request body (includes field-level errors)RATE_LIMITED(429) — too many requestsINTERNAL_ERROR(500) — unexpected server error
Rate Limiting
| Endpoint group | Limit |
|---|---|
| Auth (login, register) | 10/min per IP |
| Context serving | 1000/min per API key |
| AI pipeline (generate, analyze) | 20/min per workspace |
| CRUD mutations | 200/min per user |
| SSE connections | 10 concurrent per user |
Rate limit headers on every response:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1709740860
Pagination
List endpoints that return many items use cursor-based pagination:
GET /workspaces/{wid}/documents?limit=20&cursor=doc_abc123
Response: 200 {
documents: [...],
nextCursor: "doc_xyz789", // null if no more
total: 142
}
Default limit: 20. Max limit: 100.