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 token
  • FORBIDDEN (403) — insufficient role
  • NOT_FOUND (404) — resource doesn't exist
  • CONFLICT (409) — slug already taken, duplicate member
  • VALIDATION_ERROR (422) — invalid request body (includes field-level errors)
  • RATE_LIMITED (429) — too many requests
  • INTERNAL_ERROR (500) — unexpected server error

Rate Limiting

Endpoint groupLimit
Auth (login, register)10/min per IP
Context serving1000/min per API key
AI pipeline (generate, analyze)20/min per workspace
CRUD mutations200/min per user
SSE connections10 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.