API Reference¶
The OpenLearning API is a FastAPI application serving at http://localhost:8000. Interactive Swagger docs are available at /api/docs.
Endpoints¶
GET /api/health¶
Lightweight health check with database connectivity probe.
Response (200):
{"status": "ok", "database": null}
Response (503 — database unreachable):
{"status": "degraded", "database": "unreachable"}
GET /api/auth/github¶
Redirect to GitHub OAuth authorization. Starts the login flow.
Query parameter: redirect — path to redirect after login (default: /)
Response (302): Redirects to GitHub's authorization page.
Response (501 — GitHub OAuth not configured):
{"detail": "GitHub OAuth is not configured"}
GET /api/auth/github/callback¶
Handle the GitHub OAuth callback. Exchanges the authorization code for an access token, upserts the user in the database, and sets an httpOnly JWT cookie.
Query parameters:
| Parameter | Required | Description |
|---|---|---|
code |
Yes | OAuth authorization code from GitHub |
state |
Yes | HMAC-signed state parameter for CSRF protection |
Response (302): Redirects to the frontend with an access_token cookie set.
POST /api/auth/register¶
Register a new account with email and password. Sets an httpOnly JWT cookie on success.
Request: RegisterRequest
{
"email": "user@example.com",
"password": "securepassword"
}
Response (200):
{"ok": true}
Sets an access_token httpOnly cookie.
Response (400 — password validation failure):
{"detail": "Password must be between 8 and 128 characters"}
Response (409 — duplicate email):
{"detail": "An account with this email already exists"}
Response (422 — invalid email format):
Standard FastAPI validation error.
POST /api/auth/login¶
Authenticate with email and password. Sets an httpOnly JWT cookie on success.
Request: LoginRequest
{
"email": "user@example.com",
"password": "securepassword"
}
Response (200):
{"ok": true}
Sets an access_token httpOnly cookie.
Response (401 — invalid credentials):
{"detail": "Invalid email or password"}
GET /api/auth/me¶
Requires authentication. Returns 401 without a valid JWT cookie.
Return the current user's profile.
Response: AuthMeResponse
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"displayName": "octocat",
"avatarUrl": "https://avatars.githubusercontent.com/u/1?v=4",
"hasApiKey": false,
"email": null
}
POST /api/auth/logout¶
Clear the auth cookie.
Response (200):
{"ok": true}
POST /api/auth/api-key¶
Requires authentication. Returns 401 without a valid JWT cookie.
Store an encrypted API key for the current user.
Request: ApiKeySetRequest
{
"apiKey": "sk-ant-..."
}
Response (200):
{"ok": true}
GET /api/auth/api-key¶
Requires authentication. Returns 401 without a valid JWT cookie.
Return a masked preview of the stored API key.
Response: ApiKeyResponse
{
"apiKeyPreview": "sk-...ab12"
}
Response (204 — no API key stored):
No content body.
DELETE /api/auth/api-key¶
Requires authentication. Returns 401 without a valid JWT cookie.
Remove the stored API key for the current user. Idempotent — succeeds even if no key is stored.
Response (200):
{"ok": true}
POST /api/auth/validate-key¶
Requires authentication. Returns 401 without a valid JWT cookie.
Validate an Anthropic API key without storing it. Calls the Anthropic API to verify the key is functional.
Request: ApiKeySetRequest
{
"apiKey": "sk-ant-..."
}
Response: ValidateKeyResponse
{"valid": true, "error": null}
Response (invalid key):
{"valid": false, "error": "Invalid API key"}
Response (rate limited):
{"valid": false, "error": "Rate limited — key may be valid"}
Auth Models¶
class AuthMeResponse(CamelModel):
user_id: str
display_name: str
avatar_url: str
has_api_key: bool
email: str | None = None
class RegisterRequest(CamelModel):
email: EmailStr
password: str
class LoginRequest(CamelModel):
email: EmailStr
password: str
class ApiKeySetRequest(CamelModel):
api_key: str
class ApiKeyResponse(CamelModel):
api_key_preview: str
class ValidateKeyResponse(CamelModel):
valid: bool
error: str | None = None
Source: backend/app/routes/auth.py
GET /api/skills¶
Returns the full skills taxonomy with categories.
Response: SkillsResponse
{
"skills": [
{
"id": "nodejs",
"name": "Node.js",
"category": "Backend",
"icon": "...",
"description": "...",
"subSkills": ["Express", "Fastify"]
}
],
"categories": ["Backend", "Frontend", "DevOps"]
}
GET /api/roles¶
Returns a list of all available roles (knowledge base domains).
Response: list[RoleSummary]
[
{
"id": "backend_engineering",
"name": "Backend Engineer",
"description": "Backend engineering concepts from junior to staff level",
"skillCount": 18,
"levels": ["junior", "mid", "senior", "staff"]
},
{
"id": "frontend_engineering",
"name": "Frontend Engineer",
"description": "Frontend engineering concepts from junior to staff level",
"skillCount": 12,
"levels": ["junior", "mid", "senior", "staff"]
}
]
GET /api/roles/{role_id}¶
Returns detailed information for a single role, including mapped skill IDs and per-level concept counts.
Path parameter: role_id — the domain identifier (e.g., backend_engineering)
Response (200): RoleDetail
{
"id": "backend_engineering",
"name": "Backend Engineer",
"description": "Backend engineering concepts from junior to staff level",
"mappedSkillIds": ["nodejs", "python", "java", "go", "rest-api", "graphql", "..."],
"levels": [
{ "name": "junior", "conceptCount": 13 },
{ "name": "mid", "conceptCount": 15 },
{ "name": "senior", "conceptCount": 17 },
{ "name": "staff", "conceptCount": 15 }
]
}
Response (404 — unknown role):
{ "detail": "Role not found: unknown_role" }
GET /api/roles/{role_id}/concepts¶
Return concepts for a role up to a given level, topologically sorted by prerequisites.
Path parameter: role_id — the domain identifier (e.g., backend_engineering)
Query parameter: level — target level (default: "mid"). One of junior, mid, senior, staff.
Response (200): RoleConceptsResponse
{
"concepts": [
{
"id": "http_fundamentals",
"displayName": "HTTP Fundamentals",
"level": "junior",
"prerequisites": []
},
{
"id": "rest_api_design",
"displayName": "REST API Design",
"level": "mid",
"prerequisites": ["http_fundamentals"]
}
]
}
Response (400 — invalid level):
{"detail": "Invalid level: unknown"}
POST /api/assessment/start¶
Requires authentication. Returns 401 without a valid JWT cookie.
Requires API key. Returns 400 if the user has not configured an Anthropic API key.
Start a new assessment session. Returns the first assessment question.
Request body:
{
"skillIds": ["nodejs", "rest-api", "sql"],
"targetLevel": "mid",
"roleId": "backend_engineering",
"thoroughness": "standard"
}
| Field | Type | Required | Description |
|---|---|---|---|
skillIds |
list[string] | Yes | Skill IDs to assess |
targetLevel |
string | No (default: "mid") |
Target career level |
roleId |
string | No | Role/domain ID — when provided, bypasses skill-to-domain mapping and uses the role's knowledge base directly |
thoroughness |
string | No (default: "standard") |
Assessment depth: "quick", "standard", or "thorough". Controls max questions per topic |
Response: AssessmentStartResponse
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"question": "Can you explain what HTTP status codes are and give some examples?",
"estimatedQuestions": 20
}
Response (400 — no API key configured):
{"detail": "No API key configured. Please add your Anthropic API key in Settings."}
POST /api/assessment/{session_id}/respond¶
Requires authentication. Returns 401 without a valid JWT cookie.
Requires API key. Returns 400 if the user has not configured an Anthropic API key.
Submit an answer and receive the next question (or completion).
Request body:
{
"response": "HTTP is a stateless protocol that uses request-response pairs..."
}
Response (SSE stream): Each event is a line of the form data: <payload>\n\n.
| Signal | Payload | Description |
|---|---|---|
| (plain text) | The question text | Next question streamed as plain text |
[META] + JSON |
{"type":"assessment","topics_evaluated":3,"total_questions":12,"max_questions":25} |
Assessment progress metadata |
[ASSESSMENT_COMPLETE] |
— | Pipeline finished; scores follow |
| (fenced JSON) | {"scores": ProficiencyScoreOut[]} JSON object |
Proficiency scores wrapped in a scores key, inside markdown code fences (sent after [ASSESSMENT_COMPLETE]) |
[DONE] |
— | Stream complete |
[ERROR] + JSON |
{"status": 429, "detail": "Rate limit reached.", "retryAfter": "30"} |
Structured error with status code, message, and optional retry-after |
Response (400 — no API key configured):
{"detail": "No API key configured. Please add your Anthropic API key in Settings."}
Response (410 — session timed out):
{"detail": "Session has timed out"}
Sessions are marked as timed out after 30 minutes of inactivity. Once timed out, no further responses can be submitted.
GET /api/assessment/{session_id}/graph¶
Get the current knowledge graph for an assessment session.
Response: KnowledgeGraphOut
{
"nodes": [
{
"concept": "http_fundamentals",
"confidence": 0.85,
"bloomLevel": "apply",
"prerequisites": []
}
]
}
GET /api/assessment/{session_id}/report¶
Get the full assessment report. Stores results in the database (idempotent).
Response: AssessmentReportResponse
{
"knowledgeGraph": {
"nodes": [
{ "concept": "http_fundamentals", "confidence": 0.85, "bloomLevel": "apply", "prerequisites": [] }
]
},
"gapAnalysis": {
"overallReadiness": 72,
"summary": "Strong foundations with gaps in distributed systems and security.",
"gaps": [
{ "skillId": "distributed_systems", "skillName": "Distributed Systems", "currentLevel": 30, "targetLevel": 80, "gap": 50, "priority": "critical", "recommendation": "Focus on distributed systems fundamentals." }
]
},
"learningPlan": {
"summary": "Focus on distributed systems and security fundamentals.",
"totalHours": 24.0,
"phases": [
{
"phaseNumber": 1,
"title": "Foundations",
"concepts": ["networking", "distributed_systems"],
"rationale": "Build prerequisite knowledge first.",
"resources": [{ "type": "article", "title": "Distributed Systems Primer", "url": null }],
"estimatedHours": 8.0
}
]
},
"proficiencyScores": [
{ "skillId": "http_fundamentals", "skillName": "Http Fundamentals", "score": 85, "confidence": 0.85, "reasoning": "Strong understanding demonstrated" }
]
}
GET /api/assessment/{session_id}/export¶
Export the full assessment report as a formatted Markdown file.
- If the assessment is complete, data is read from the database.
- If the assessment is still in progress, data falls back to the live graph state (sections without data render gracefully with fallback text).
Response (200):
- Content-Type:
text/markdown - Content-Disposition:
attachment; filename="assessment-{session_id[:8]}.md" - Body: Formatted Markdown with sections: Proficiency Scores, Knowledge Map, Knowledge Gaps, Learning Plan.
Response (404 — session not found):
{"detail": "Session not found"}
GET /api/assessment/{session_id}/resume¶
Requires authentication. Returns 401 without a valid JWT cookie.
Requires API key. Returns 400 if the user has not configured an Anthropic API key.
Resume an active assessment session that was interrupted or left incomplete. Loads the pending question from the LangGraph checkpoint.
Response (200):
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"question": "Can you explain how React hooks work?"
}
Response (403 — not your session):
{"detail": "Not your session"}
Response (404 — session not found):
{"detail": "Session not found"}
Response (409 — session already completed or no pending question):
{"detail": "Session already completed"}
Response (410 — session timed out):
{"detail": "Session has timed out"}
GET /api/user/assessments¶
Requires authentication. Returns 401 without a valid JWT cookie.
List all assessment sessions for the authenticated user, sorted by creation date (newest first). Includes basic result data for completed sessions.
Response (200):
[
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"skillIds": ["react", "typescript"],
"targetLevel": "mid",
"roleId": "frontend_engineering",
"roleName": "Frontend Engineer",
"createdAt": "2026-03-20T10:30:00Z",
"completedAt": "2026-03-20T11:45:00Z",
"overallReadiness": 72,
"skillCount": 2
},
{
"sessionId": "660e8400-e29b-41d4-a716-446655440001",
"status": "active",
"skillIds": ["python"],
"targetLevel": "mid",
"roleId": null,
"roleName": null,
"createdAt": "2026-03-22T09:00:00Z",
"completedAt": null,
"overallReadiness": null,
"skillCount": 1
}
]
DELETE /api/user/assessments/{session_id}¶
Requires authentication. Returns 401 without a valid JWT cookie.
Delete an assessment session and all associated data (results, materials). Only the session owner can delete it.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
session_id |
string | ID of the session to delete |
Response (204): No content.
Errors:
| Status | Detail |
|---|---|
| 403 | Not your session |
| 404 | Session not found |
POST /api/gap-analysis¶
Requires authentication. Returns 401 without a valid JWT cookie.
Requires API key. Returns 400 if the user has not configured an Anthropic API key.
Generate a gap analysis from proficiency scores.
Request: GapAnalysisRequest
{
"proficiencyScores": [
{
"skillId": "nodejs",
"skillName": "Node.js",
"score": 65,
"confidence": 0.8,
"reasoning": "Strong fundamentals, gaps in advanced patterns"
}
]
}
Response: GapAnalysis
{
"overallReadiness": 72,
"summary": "Solid foundation with gaps in distributed systems and security.",
"gaps": [
{
"skillId": "microservices",
"skillName": "Microservices",
"currentLevel": 45,
"targetLevel": 80,
"gap": 35,
"priority": "critical",
"recommendation": "Focus on service decomposition and inter-service communication patterns."
}
]
}
Priority levels: critical (gap > 40), high (gap > 25), medium (gap > 10), low (gap <= 10).
Response (400 — no API key configured):
{"detail": "No API key configured. Please add your Anthropic API key in Settings."}
POST /api/learning-plan¶
Requires authentication. Returns 401 without a valid JWT cookie.
Requires API key. Returns 400 if the user has not configured an Anthropic API key.
Generate a personalized learning plan from gap analysis.
Request: LearningPlanRequest
{
"gapAnalysis": {
"overallReadiness": 72,
"summary": "...",
"gaps": [...]
}
}
Response: LearningPlan
{
"summary": "A 6-week plan targeting distributed systems and security gaps.",
"totalHours": 48,
"phases": [
{
"phaseNumber": 1,
"title": "Foundations",
"concepts": ["microservices", "http_fundamentals"],
"rationale": "Build foundational understanding before tackling distributed systems.",
"resources": [
{
"type": "article",
"title": "Microservices Fundamentals",
"url": "https://microservices.io/patterns"
},
{
"type": "video",
"title": "HTTP Deep Dive",
"url": null
}
],
"estimatedHours": 12
}
]
}
Response (400 — no API key configured):
{"detail": "No API key configured. Please add your Anthropic API key in Settings."}
GET /api/materials/{session_id}¶
Requires authentication. Returns 401 without a valid JWT cookie.
Retrieve generated learning materials for a completed assessment session. Materials are generated automatically in the background when an assessment completes.
Path parameter: session_id — the assessment session UUID
Response (200): MaterialsResponse
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"materials": [
{
"conceptId": "http_fundamentals",
"domain": "backend_engineering",
"bloomScore": 0.91,
"qualityScore": 0.88,
"iterationCount": 1,
"qualityFlag": null,
"material": {
"concept_id": "http_fundamentals",
"target_bloom": 2,
"sections": [
{"type": "explanation", "title": "What HTTP does", "body": "..."},
{"type": "code_example", "title": "HTTP Request", "body": "...", "codeBlock": "..."},
{"type": "quiz", "title": "Check understanding", "body": "...", "answer": "..."}
]
},
"generatedAt": "2026-03-23T12:00:00Z"
}
]
}
An empty materials list indicates the pipeline is still running.
Response (404 — session not found):
{"detail": "Session not found"}
Source: backend/app/routes/materials.py
Anthropic Error Responses¶
When the backend encounters an Anthropic SDK exception, it maps it to a structured HTTP error:
| Anthropic Exception | HTTP Status | Message |
|---|---|---|
AuthenticationError |
401 | Your API key is invalid or has been revoked. Please update it in settings. |
RateLimitError |
429 | Rate limit reached. Please wait a moment and try again. (Retry-After header included) |
APIConnectionError |
502 | Unable to reach the AI service. Please try again shortly. |
APITimeoutError |
504 | The AI service timed out. Please try again. |
InternalServerError |
502 | The AI service encountered an error. Please try again. |
Applies to /assessment/start, /assessment/{id}/respond, /gap-analysis, /learning-plan. For SSE streams, errors arrive as data: [ERROR]{json}\n\n instead of HTTP status codes.
Source: backend/app/main.py (register_anthropic_error_handlers), backend/app/services/ai.py (classify_anthropic_error)
Assessment Flow¶
The full assessment flow involves multiple API calls:
sequenceDiagram
participant Client
participant GitHub
participant API
participant LangGraph
Client->>API: GET /auth/github
API-->>Client: 302 → GitHub OAuth
Client->>GitHub: Authorize
GitHub-->>Client: Callback with code
Client->>API: GET /auth/github/callback
API-->>Client: Set JWT cookie, redirect
Client->>API: POST /assessment/start
API->>LangGraph: Initialize pipeline
LangGraph-->>API: First assessment question (interrupt)
API-->>Client: JSON: AssessmentStartResponse
Client->>API: POST /assessment/{id}/respond
API->>LangGraph: Resume with answer
LangGraph-->>API: Next assessment question (interrupt)
API-->>Client: SSE: assessment question
Note over Client,LangGraph: ...assessment loop continues...
Client->>API: POST /assessment/{id}/respond
API->>LangGraph: Resume with final answer
LangGraph-->>API: Pipeline complete
API-->>Client: SSE: completion + scores
Client->>API: GET /assessment/{id}/report
API-->>Client: Full report (graph, gaps, plan)
SSE Streaming¶
The /assessment/{id}/respond endpoint uses Server-Sent Events (SSE) for streaming responses. The frontend receives events as they're generated, enabling real-time display of questions and progress updates. Note that /assessment/start returns a regular JSON response, not SSE.
Swagger Documentation¶
For the full interactive API documentation with request/response schemas, run the backend and visit: