Base URL: http://localhost:4000/api/v1 (dev)
Interactive docs: http://localhost:4000/api/docs (Swagger UI)
Frontend integration guide: FRONTEND_INTEGRATION.md — full E2E flow from bootstrap through course completion.
All user-facing endpoints require Clerk JWT unless noted.
Authorization: Bearer <clerk-session-jwt>Backend verifies via @clerk/backend verifyToken(). Resolves clerkUserId → profileId via shared Profile table.
Reads Server.learningGoal, learningReason, and LearningMaterial[] from the shared DB. Requires both goal and materials. Writes to shared Curriculum/Module/Quiz tables and backend Concept bank + ServerLearningMeta.
POST /servers/:serverId/bootstrap
Authorization: Bearer <clerk-jwt>Response 202 Accepted (default async)
{
"jobId": "uuid",
"status": "generating"
}Response 200 when BOOTSTRAP_SYNC=true on the backend
{
"curriculumId": "clx...",
"moduleCount": 4,
"conceptCount": 9,
"goalCriteria": ["...", "..."],
"summary": "Personalized path for ..."
}| Code | Meaning |
|---|---|
| 400 | Missing goal or materials |
| 403 | Not server admin |
| 409 | Already GENERATING or already READY |
Lightweight lifecycle poll. Use while bootstrap runs; does not return lesson content.
GET /servers/:serverId/curriculum
Authorization: Bearer <clerk-jwt>Response 200
{
"serverId": "clx...",
"status": "READY",
"summary": "4-module path covering ...",
"modules": [
{ "id": "clx...", "order": 1, "title": "Foundations", "progressStatus": "AVAILABLE" }
],
"conceptCount": 9,
"goalCriteria": ["Learner can ...", "..."]
}Curriculum status enum: PENDING | GENERATING | READY | FAILED
Module progressStatus enum: LOCKED | AVAILABLE | IN_PROGRESS | COMPLETED
Returns nested modules, concepts, gate quizzes, and final exam after bootstrap completes.
GET /servers/:serverId/curriculum/content
Authorization: Bearer <clerk-jwt>Response 200 when status === READY — see Swagger CurriculumContentResponseDto for full shape.
Response 409 Conflict when not ready
{
"status": "GENERATING",
"message": "Curriculum not ready yet"
}| status | message |
|---|---|
PENDING / GENERATING |
Curriculum not ready yet |
FAILED |
Bootstrap failed; retry POST /bootstrap |
Answer redaction: Non-admin members receive learner-safe payloads (no correct flags, solutions, or rubrics). Server admins receive full content.
Called by the frontend after persisting a user message. Returns immediately; agent replies are pushed via internal webhook.
POST /conversations/:conversationId/process
Authorization: Bearer <clerk-jwt>
Content-Type: application/json
{
"serverId": "clx...",
"memberId": "clx...",
"profileId": "clx...",
"message": {
"id": "clx...",
"content": "Let's start",
"type": "TEXT"
},
"conversationType": "AGENT"
}Response 202 Accepted
{ "jobId": "uuid", "status": "processing" }Other statuses (still 202):
| status | Meaning |
|---|---|
processing |
Workflow enqueued |
curriculum_not_ready |
Bootstrap not finished — no workflow runs |
duplicate_ignored |
Same conversationId called within 5 seconds |
Rate limit: 10 requests/minute per client.
| Code | Meaning |
|---|---|
| 401 | Invalid/missing JWT |
| 403 | Not a member of server |
| 422 | Invalid payload |
Note: conversationId is used as the workflow session key. The backend does not validate it against the frontend DB.
Returns current workflow step and optional nextAction for UI chips/buttons.
GET /conversations/:conversationId/session?serverId=clx...
Authorization: Bearer <clerk-jwt>Response 200
{
"conversationId": "clx...",
"serverId": "clx...",
"currentConceptId": "clx...",
"currentConceptTitle": "Solving linear equations",
"step": "QUICK_CHECK",
"nextAction": {
"type": "MCQ",
"questionId": "clx...",
"prompt": "What is the first step when solving 2x + 4 = 10?"
},
"progress": {
"conceptsCompleted": 2,
"conceptsTotal": 12
}
}Workflow step enum: STUDY | QUICK_CHECK | PRACTICE | EXPLAIN_BACK | MICRO_DRILL | CONCEPT_COMPLETE | MODULE_GATE | GOAL_VERIFICATION | MOCK_INTERVIEW | INTERVIEW_DEBRIEF | COURSE_COMPLETE | FREE_CHAT
Both endpoints resume the Mastra workflow synchronously. On success, agent messages are pushed to the frontend via the internal webhook (same as /process).
POST /assessments/submit
Authorization: Bearer <clerk-jwt>
Content-Type: application/json
{
"conversationId": "clx...",
"serverId": "clx...",
"memberId": "clx...",
"questionId": "clx...",
"questionType": "COMPREHENSION",
"answer": "B"
}| questionType | answer format |
|---|---|
COMPREHENSION |
MCQ choice ID (e.g. "A", "B") |
PRACTICE |
Free-text solution |
Response 200
{
"status": "processed",
"messageCount": 3
}Follow-up agent/system messages arrive via webhook.
POST /assessments/explain-back
Authorization: Bearer <clerk-jwt>
Content-Type: application/json
{
"conversationId": "clx...",
"serverId": "clx...",
"memberId": "clx...",
"conceptId": "clx...",
"answer": "You subtract 4 from both sides first because..."
}Response 200
{
"passed": true,
"feedback": "Good — you identified the isolation step correctly.",
"scoreDelta": 15,
"nextStep": "CONCEPT_COMPLETE"
}On pass, agent messages are also webhook-delivered. On fail, only synchronous feedback is returned; learner returns to STUDY.
GET /progress/:serverId
Authorization: Bearer <clerk-jwt>Response 200
{
"serverId": "clx...",
"courseScore": 142,
"rank": "INTERMEDIATE",
"concepts": [
{
"id": "clx...",
"title": "Linear equations",
"mastery": 0.85,
"status": "COMPLETED",
"nextReviewAt": "2026-07-20T00:00:00.000Z"
}
],
"modules": [
{
"id": "clx...",
"title": "Foundations",
"order": 1,
"status": "IN_PROGRESS",
"score": null
}
],
"currentModuleId": "clx...",
"goalMet": null,
"certificateStage": null,
"interviewStatus": "not_applicable",
"weakTopics": ["LINEAR_ADD_BEFORE_ISOLATE"],
"strongTopics": ["isolation-basic"]
}| Field | Notes |
|---|---|
rank |
BEGINNER | INTERMEDIATE | ADVANCED | EXPERT |
goalMet |
null until COURSE_COMPLETE step |
certificateStage |
provisional | final | none |
interviewStatus |
not_applicable | not_started | in_progress | passed | failed |
Flat ordered list — no study units or quiz content. Use webhook messages or GET /curriculum/content for full lesson bodies.
GET /courses/:serverId/concepts
Authorization: Bearer <clerk-jwt>Response 200
{
"serverId": "clx...",
"subject": "Algebra I",
"agentHandle": "@AlgebraI",
"concepts": [
{ "id": "clx...", "slug": "linear-equations", "title": "Linear equations", "order": 1 }
]
}GET /healthNo auth required.
Response 200
{
"status": "ok",
"database": "connected",
"mastra": "ready"
}status is degraded when the database is unreachable.
Implemented on the frontend (not this backend). Documented here for contract alignment. See FRONTEND_INTEGRATION.md § Phase 5.
POST {CYCY_URL}/api/internal/agent-response
X-Internal-Secret: <CYCY_INTERNAL_SECRET>
Content-Type: application/json
{
"conversationId": "clx...",
"messages": [
{
"authorType": "AI_AGENT",
"content": "Let's start with linear equations...",
"metadata": {
"type": "EXPLANATION",
"conceptId": "clx...",
"step": "STUDY"
}
},
{
"authorType": "SYSTEM",
"content": "Quick check",
"metadata": {
"type": "MCQ",
"questionId": "clx...",
"choices": [
{ "id": "A", "text": "Add 4 to both sides" },
{ "id": "B", "text": "Subtract 4 from both sides" }
],
"step": "QUICK_CHECK"
}
}
]
}Expected response 201
{ "saved": 2, "emitted": true }The frontend must persist each message and emit its real-time channel (e.g. Socket.io chat:{conversationId}:messages).
| authorType | UI treatment |
|---|---|
USER |
Standard user bubble |
AI_AGENT |
Agent avatar + course agent name |
SYSTEM |
Structured UI by metadata.type |
| metadata.type | Frontend component |
|---|---|
GREETING |
Welcome bubble |
EXPLANATION |
Markdown study content |
STEP_CHIP |
Study → Quick check → Practice chips |
MCQ |
Inline multiple-choice buttons |
PRACTICE |
Problem prompt + text input |
EXPLAIN_BACK |
Explain-your-reasoning prompt |
GAP_CALLOUT |
Misconception highlight card |
FEEDBACK |
Inline grading feedback |
MOTIVATION |
Encouragement + XP |
CONCEPT_COMPLETE |
Concept completion celebration |
MODULE_COMPLETE |
Module done banner |
MODULE_GATE |
Gate quiz prompt |
GOAL_VERIFICATION |
Goal pass/fail summary |
CERTIFICATE |
Certificate + verification code |
MOCK_INTERVIEW |
Interview question |
INTERVIEW_DEBRIEF |
Interview result |
COURSE_COMPLETE |
Course finished banner |
FREE_CHAT |
Off-loop tutor answer |
ERROR |
Error banner |
PROGRESS |
Progress update |
MCQ choices in webhook payloads never include correct: true — grading is server-side only.
| Variable | Purpose |
|---|---|
DATABASE_URL |
Shared PostgreSQL with frontend |
CLERK_SECRET_KEY |
JWT verification |
OPENAI_API_KEY |
LLM (OpenRouter keys sk-or-* auto-route to OpenRouter) |
OPENAI_BASE_URL |
Optional override (default: OpenAI or OpenRouter auto-detect) |
OPENAI_MODEL |
Optional model ID |
CYCY_URL |
Webhook target base URL |
CYCY_INTERNAL_SECRET |
Webhook auth header |
BOOTSTRAP_SYNC |
true = synchronous bootstrap response |
PUBLIC_URL / RENDER_EXTERNAL_URL |
Swagger server URL in production |
{
"statusCode": 422,
"message": "Validation failed",
"errors": [{ "field": "answer", "message": "Required" }]
}| Method | Path | Auth |
|---|---|---|
| GET | /health |
None |
| POST | /servers/:serverId/bootstrap |
Clerk |
| GET | /servers/:serverId/curriculum |
Clerk |
| GET | /servers/:serverId/curriculum/content |
Clerk |
| POST | /conversations/:conversationId/process |
Clerk |
| GET | /conversations/:conversationId/session |
Clerk |
| POST | /assessments/submit |
Clerk |
| POST | /assessments/explain-back |
Clerk |
| GET | /progress/:serverId |
Clerk |
| GET | /courses/:serverId/concepts |
Clerk |