Base URL: /api/v1/
Auth: All endpoints require a valid Clerk JWT in the Authorization: Bearer <token> header unless noted otherwise.
Content-Type: application/json
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/up |
None | Rails health check (200 if app is running) |
GET |
/health |
None | Database-free process health check used by Render: {"status":"ok"} |
GET |
/api/v1/ready |
None | Manual PostgreSQL and background-worker readiness diagnostic |
Syncs an invited user record from the Clerk JWT. Production access is invite-only: the Clerk identity must already match an active CSG user by Clerk ID or email address.
Request: No body required — user info is extracted from the JWT.
Response:
{
"user": {
"id": 1,
"email": "student@example.com",
"first_name": "Kevin",
"last_name": "Santos",
"role": "student",
"github_username": "kevinsantos",
"avatar_url": "https://...",
"last_sign_in_at": "2026-04-07T10:00:00Z"
}
}Access-denied response (403):
{
"error": "This account does not have access to CSG Learning yet. Ask a Code School administrator to invite this email address.",
"code": "account_not_authorized"
}Archived users receive the same status with code: "account_archived". Missing, invalid, and expired Clerk JWTs remain 401 responses.
Creates a one-time, 60-second Clerk sign-in link for an allowlisted responsive-web destination. Mobile uses this for browser-shaped tools without placing the device session JWT in a URL. All signed-in users may hand off to numeric lesson and module paths. Staff may also hand off to allowlisted student workspaces, submission/help/intervention records, cohort operations, grading, content, and team destinations. For example:
{ "destination": "/lessons/42" }The response contains a Clerk Account Portal URL whose post-authentication redirect is resolved from PUBLIC_FRONTEND_URL. External, malformed, and unsupported destinations are rejected.
Returns the current authenticated user's profile.
Updates the current user's profile.
Body:
{
"user": {
"first_name": "Kevin",
"last_name": "Santos",
"github_username": "kevinsantos"
}
}Returns role-appropriate dashboard data.
Student response: Current cohort, pinned/unread announcements, unread notification count, modules with progress and unlock state, next lesson, current redo items, latest passing grades, resources, and upcoming office hours.
Staff response: All active cohorts with student progress and activity signals. Each student summary includes ungraded_count and redo_count; each cohort and the backward-compatible top-level summary include the total ungraded count.
{
"dashboard": {
"user": { "id": 1, "full_name": "Leon Shimizu", "role": "admin" },
"cohort": { "id": 3, "name": "Cohort 3", "start_date": "2026-03-31" },
"students": [
{
"user_id": 2,
"full_name": "Kevin Santos",
"progress_percentage": 45.5,
"completed_blocks": 58,
"total_blocks": 128,
"last_activity_at": "2026-04-06T15:30:00Z",
"ungraded_count": 2,
"redo_count": 1,
"enrollment_status": "active"
}
],
"ungraded_count": 5,
"cohorts": [ "..." ]
}
}Returns the authenticated student’s shared web/native This Week projection. Staff receive 403; students without an active enrollment receive { "enrolled": false }.
The Pacific/Guam Monday–Sunday projection contains:
- required and optional lessons, including incomplete work carried forward from earlier weeks;
- completion/open/upcoming/closed state and any submission close instant;
- the latest open redo per assignment;
- one-time or recurring live-class and office-hour occurrences;
- the next lesson unlocks and up to three incomplete class recordings;
- content-free summary counts used by
weekly_plan_viewedanalytics.
Clients should link lesson, recording, and meeting actions from the provided typed IDs/URLs and cache the projection only inside the signed-in user’s existing scoped cache.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/help_requests |
Signed-in user | Students list their own requests; staff list all. Optional cohort_id, valid status, and valid context_type filters apply; staff may also filter by student_id. |
GET |
/api/v1/help_requests/:id |
Owner student / Staff | Return one stable help-request record with its authorized student, cohort, learning context, ownership, status, and response relationships. Requests outside the caller's scope return 404. |
POST |
/api/v1/help_requests |
Student | Create one active request for an authorized lesson, exercise, uploaded recording, or legacy recording. Duplicate active context returns the existing record with created: false. |
PATCH |
/api/v1/help_requests/:id |
Owner student / Staff | Students may cancel active requests. Staff may acknowledge or resolve; resolution requires a nonblank staff_response. |
GET |
/api/v1/support_queue |
Staff | Return active requests, recent resolutions, summary counts, and explainable redo/ungraded/inactivity student signals. |
Create input is nested under help_request and includes cohort_id, context_type (lesson, exercise, or recording), context_source (primary or legacy recordings only), numeric context_id, category, urgency, and message. The server re-resolves the context label/path and rechecks active enrollment, curriculum, assignment, release, and recording ownership; clients cannot choose those display values.
States are open, acknowledged, resolved, and canceled. Terminal records cannot be reopened. Notifications omit authored request text. See SUPPORT_WORKFLOW.md for the product and operating contract.
Returns one normalized items list. Students receive published recordings across their active cohort enrollments; staff receive draft and published recordings across all active and upcoming cohorts. Each first-class item has a numeric id, stable item_key, cohort, status, recording date, media metadata, and source (uploaded, youtube, vimeo, loom, direct, or external). Students also receive their watch progress for every first-class source. Unmigrated settings links may temporarily appear with a synthetic string ID and do not support progress.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/v1/cohorts/:cohort_id/recordings |
Staff | Creates an uploaded recording from managed-upload metadata, or a hosted recording from an HTTPS source_url |
PATCH |
/api/v1/cohorts/:cohort_id/recordings/:id |
Staff | Updates recording details, status, and the URL/provider of a hosted recording |
DELETE |
/api/v1/cohorts/:cohort_id/recordings/:id |
Staff | Deletes a recording; managed uploaded media is cleaned up asynchronously |
GET |
/api/v1/cohorts/:cohort_id/recordings/:id/stream_url |
Staff or active cohort member | Returns a two-hour stream_url and ISO 8601 expires_at for uploaded media; hosted recordings return 422 |
PATCH |
/api/v1/watch_progress |
Staff or active cohort member | Saves monotonic watch time and resume position; completes at 90% |
GET |
/api/v1/content_blocks/:id/video_stream |
Authorized lesson viewer | Returns a two-hour lesson-video stream_url, expires_at, and current progress |
PATCH |
/api/v1/content_blocks/:id/video_progress |
Authorized lesson viewer | Saves authoritative lesson-video progress |
POST |
/api/v1/content_blocks/:id/video_presign |
Staff | Presigns a normal upload for an existing lesson video block |
POST |
/api/v1/video_presign |
Staff | Presigns a normal upload before a lesson video block exists |
POST |
/api/v1/uploads/multipart/initiate |
Staff | Starts a large recording or lesson-video upload using cohort_id or content_block_id |
POST |
/api/v1/uploads/multipart/part_url |
Staff | Presigns one validated multipart upload part |
POST |
/api/v1/uploads/multipart/complete |
Staff | Completes a multipart upload from ordered part ETags |
DELETE |
/api/v1/uploads/multipart/abort |
Staff | Aborts an unfinished multipart upload |
DELETE |
/api/v1/uploads/abandon |
Staff | Deletes an unreferenced managed upload after an editor or create failure |
GET |
/api/v1/watch_progress/student/:user_id |
Staff | Returns recording progress for the student's active enrollments, or the exact enrolled cohort when cohort_id is provided |
GET |
/api/v1/watch_progress/student/:user_id/lesson_videos |
Staff | Returns curriculum lesson-video progress for active enrollments, or the exact enrolled cohort when cohort_id is provided |
Signed URLs are temporary secrets. Clients should renew before expires_at, avoid logging or persisting them, and retain playback position when renewing a URL for the same source. Replacing the hosted video resets watch progress for the new file.
Students receive resources/links for their active cohort. Staff receive resources across all active and upcoming cohorts; each item includes cohort_id and cohort_name and uses a cross-cohort-safe synthetic ID.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/users |
Staff | List users; supports role, admin-only include_archived, and include_enrollments |
GET |
/api/v1/users/:id |
Staff | Show user details |
POST |
/api/v1/users |
Admin | Create user (sends Clerk invite) |
PATCH |
/api/v1/users/:id |
Admin | Update user |
DELETE |
/api/v1/users/:id |
Admin | Archive user; hard-delete only unused pending invites |
POST |
/api/v1/users/:id/resend_invite |
Admin | Resend Clerk invitation email |
PATCH |
/api/v1/users/:id/unarchive |
Admin | Restore archived user; re-sends invite when still pending |
Archived users are hidden from default user lists, team management, active cohort/member lists, messaging pickers, and notification recipients. Their historical messages, announcements, and records remain attached for audit/history. Admins can pass include_archived=true to GET /api/v1/users when they need to inspect archived accounts.
Create body:
{
"user": {
"email": "student@example.com",
"first_name": "Kevin",
"last_name": "Santos",
"role": "student"
}
}| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/curricula |
Staff | List all curricula |
GET |
/api/v1/curricula/:id |
Staff | Show curriculum with modules |
POST |
/api/v1/curricula |
Admin | Create curriculum |
PATCH |
/api/v1/curricula/:id |
Admin | Update curriculum |
DELETE |
/api/v1/curricula/:id |
Admin | Delete curriculum |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/curricula/:curriculum_id/modules |
Staff | List modules in curriculum |
POST |
/api/v1/curricula/:curriculum_id/modules |
Admin | Create module |
GET |
/api/v1/modules/:id |
Authenticated | Show module with lessons |
PATCH |
/api/v1/modules/:id |
Admin | Update module |
DELETE |
/api/v1/modules/:id |
Admin | Delete module |
Create/Update body:
{
"curriculum_module": {
"name": "Prework",
"module_type": "prework",
"position": 1,
"day_offset": 0,
"total_days": 35,
"schedule_days": "weekdays",
"description": "Pre-class exercises and videos"
}
}| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/modules/:module_id/lessons |
Staff | List lessons in module |
POST |
/api/v1/modules/:module_id/lessons |
Staff | Create lesson |
POST |
/api/v1/modules/:module_id/exercises |
Staff | Create exercise lesson (shorthand) |
GET |
/api/v1/lessons/:id |
Authenticated | Show lesson with content blocks |
PATCH |
/api/v1/lessons/:id |
Staff | Update lesson |
PATCH |
/api/v1/lessons/:id/editor |
Staff | Atomically update lesson details, video/exercise blocks, learning design, and hosted-video metadata using base_updated_at |
DELETE |
/api/v1/lessons/:id |
Admin | Delete lesson |
Create body:
{
"lesson": {
"title": "Version Control",
"lesson_type": "exercise",
"position": 1,
"release_day": 0,
"required": true,
"requires_submission": true
}
}| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/lessons/:lesson_id/content_blocks |
Staff | List blocks in lesson |
POST |
/api/v1/lessons/:lesson_id/content_blocks |
Staff | Create content block |
GET |
/api/v1/content_blocks/:id |
Authenticated | Show block |
PATCH |
/api/v1/content_blocks/:id |
Staff | Update block |
DELETE |
/api/v1/content_blocks/:id |
Admin | Delete block |
Create body:
{
"content_block": {
"block_type": "exercise",
"position": 1,
"title": "Create your first repo",
"body": "<p>Instructions in HTML...</p>",
"solution": "# Solution code here",
"filename": "first_repo.rb",
"video_url": null,
"metadata": {}
}
}| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/learning_objectives?curriculum_id=:id |
Staff | List ordered curriculum objectives and success criteria |
POST |
/api/v1/learning_objectives |
Admin | Create a reusable curriculum objective and success criteria |
PATCH / DELETE |
/api/v1/learning_objectives/:id |
Admin | Update or remove an unused objective |
PATCH |
/api/v1/lessons/:lesson_id/objective_alignments |
Admin | Atomically replace the lesson's ordered objective alignments |
GET |
/api/v1/rubrics?curriculum_id=:id |
Staff | List reusable curriculum rubrics and ordered criteria |
POST |
/api/v1/rubrics |
Admin | Create a reusable curriculum rubric and ordered criteria |
PATCH / DELETE |
/api/v1/rubrics/:id |
Admin | Update or remove a rubric while preserving submitted evidence |
POST |
/api/v1/knowledge_checks/:knowledge_check_id/attempts |
Student | Record one answer and return immediate result/explanation evidence |
The atomic lesson editor accepts a retrieval_check object for its checkpoint. A check has 2–6 options, one server-held correct option, an explanation, and an optional objective from the same curriculum. Students do not receive the answer or explanation before an attempt. Correct attempts complete the checkpoint; the generic progress endpoint cannot bypass this evidence. Attempted checks are immutable, and enrollment restart snapshots and removes the student's attempts for that curriculum. Its optional video object accepts s3_video_key, s3_video_content_type, and s3_video_size; a new or replacement hosted key requires both exact metadata fields. MIME and size are revalidated before the managed object is attached, and replacing the key resets student watch progress for the new file.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/v1/transcriptions |
Any signed-in user | Create a temporary, reviewable text draft from M4A voice input |
The multipart request includes audio, surface (message, thread, help_request, or grading_feedback), and cleanup=conservative. M4A input is limited to 6 MB and five minutes; content type, file signature, and movie duration are verified. The response contains raw_text, suggested_text, verified duration_seconds, and warnings. Disabled, unconfigured, and provider-failure responses include the stable codes voice_disabled, voice_not_configured, and voice_provider_error respectively.
The endpoint is rate-limited per user, fails closed until the production feature flag is enabled, never sends or saves the draft, creates no transcript record, and does not retain uploaded audio. The destination's normal authorization and explicit Send/Resolve/Grade action remain authoritative.
| Method | Path | Auth | Description |
|---|---|---|---|
GET / POST |
/api/v1/feedback_snippets |
Staff | List active shared snippets or create one |
PATCH / DELETE |
/api/v1/feedback_snippets/:id |
Owner or admin | Edit or deactivate a snippet |
POST |
/api/v1/feedback_snippets/:id/use |
Staff | Record use of an active snippet |
Snippet text is always inserted into an editable feedback draft; it never grades, sends, or saves feedback automatically.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/announcements |
Any signed-in user | List announcements visible to the current user |
GET |
/api/v1/announcements?scope=manage |
Staff | List all announcements for staff management |
GET |
/api/v1/announcements/:id |
Visible user / Staff | Show announcement and mark its notification read for the current user |
POST |
/api/v1/announcements |
Staff | Publish or draft an announcement |
PATCH |
/api/v1/announcements/:id |
Staff | Update an announcement |
DELETE |
/api/v1/announcements/:id |
Staff | Archive an announcement |
Create body:
{
"title": "Class recording is ready",
"body": "Week 3 recording is posted in the recordings tab.",
"audience": "cohort",
"cohort_id": 3,
"status": "published",
"pinned": true,
"send_push": true
}audience may be cohort, global, or staff. send_push fans out to configured browser Web Push subscriptions and active Expo mobile device tokens for eligible recipients.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/notifications |
Any signed-in user | List current user's notifications and unread count |
PATCH |
/api/v1/notifications/:id/read |
Owner | Mark one notification read |
PATCH |
/api/v1/notifications/mark_all_read |
Any signed-in user | Mark all current-user notifications read |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/push_subscriptions/config |
Any signed-in user | Return message-notification preference plus Web Push configuration and the public VAPID key |
PATCH |
/api/v1/push_subscriptions/preferences |
Any signed-in user | Enable or disable DM email and browser-push notifications globally; works without browser push support |
POST |
/api/v1/push_subscriptions |
Any signed-in user | Store the current browser/device push subscription |
DELETE |
/api/v1/push_subscriptions |
Any signed-in user | Remove a subscription endpoint for this user |
Create body:
{
"endpoint": "https://push.example/subscription",
"keys": {
"p256dh": "browser-public-key",
"auth": "browser-auth-secret"
}
}Preference body:
{
"notifications_enabled": false
}DM email notifications are enabled by default and are queued independently of browser-push support. Disabling the preference suppresses DM and mention emails; browser subscriptions remain device-specific. Each DM email uses the notification ID as a provider idempotency key, verifies that the provider returned a delivery ID, and retries typed delivery failures. Operational logs use internal user and notification IDs without recording recipient addresses or message bodies.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/v1/mobile_push_tokens |
Any signed-in user | Register or refresh the current Expo device token |
DELETE |
/api/v1/mobile_push_tokens |
Token owner | Remove the current Expo device token at sign-out |
Register body:
{
"token": "ExpoPushToken[device-token]",
"platform": "ios",
"device_id": "optional-installation-id",
"app_version": "1.0.0"
}platform must be ios or android. A token cannot be claimed by a different signed-in user. Tokens rejected by Expo as DeviceNotRegistered are marked inactive and can be reactivated by a later successful registration.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/channels |
Any signed-in user | List channels visible to the current user with unread counts |
GET |
/api/v1/channels/:id |
Channel member / Staff | Show a channel and a window of messages |
POST |
/api/v1/channels |
Staff | Create a cohort or staff-only channel |
PATCH |
/api/v1/channels/:id |
Staff | Update channel metadata |
DELETE |
/api/v1/channels/:id |
Staff | Archive a channel |
PATCH |
/api/v1/channels/:id/read |
Channel member / Staff | Mark a channel read for the current user |
Create body:
{
"cohort_id": 3,
"name": "Class Chat",
"description": "General class discussion.",
"visibility": "cohort"
}visibility may be cohort or staff_only. A default Class Chat channel is created automatically for every cohort.
Channel and direct-conversation show endpoints accept message_limit, around_message_id, and before_message_id. Responses include chronological messages, pinned_messages, and a meta object with oldest_message_id, newest_message_id, has_older, and has_newer. This lets web and native clients open a search result in context and page backward without overlaps.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/v1/channels/:channel_id/messages |
Channel member / Staff | Post a message |
POST |
/api/v1/direct_conversations/:direct_conversation_id/messages |
Conversation member | Post a direct or group message |
GET |
/api/v1/messages/:id/thread |
Conversation member | Return the root message and its chronological replies |
PATCH |
/api/v1/messages/:id |
Author / Staff | Edit a message |
DELETE |
/api/v1/messages/:id |
Author / Staff | Soft-delete a message |
PATCH / DELETE |
/api/v1/messages/:id/pin |
Staff | Pin or unpin a message |
POST / DELETE |
/api/v1/messages/:id/reactions |
Conversation member | Add or remove a reaction |
Create body:
{
"body": "Can someone share the Zoom link?",
"parent_message_id": null,
"client_message_id": "01990f7a-2068-7e9f-b884-6a8b4bb44dd0",
"mention_user_ids": [42],
"attachments": [],
"send_push": true
}body is limited to 5,000 characters. Clients must generate a distinct client_message_id for each send intent and reuse it for every retry of that message. The identifier is scoped to the author—not the conversation—and is limited to 100 characters; a longer value returns 422 Unprocessable Entity. A first-time create returns 201 Created. A matching replay returns 200 OK with the original message, resumes any interrupted notification or realtime delivery, and does not repeat completed delivery work. The first accepted request's send_push value governs all replays.
409 Conflict is returned when an author reuses the identifier with a different body, parent message, mention set, attachment set, or conversation. Replaying an identifier whose message was subsequently deleted also returns 409 Conflict; it does not recreate or expose the deleted message. Create, replay, message-list, and realtime ActionCable payloads include the value only for the authenticated author and omit it for other viewers. Older clients may omit it.
Message deletion returns 503 Service Unavailable with a retryable error when an in-flight delivery still owns the message after the bounded wait. The message is not deleted in that case, preventing a late create event from appearing after a successful deletion response.
An inline deployment also returns a retryable 503 after a committed create or replay with a client_message_id when notification or realtime delivery is incomplete. Clients must retry the same operation and reuse the original identifier; a Solid Queue deployment can acknowledge the committed write because its recurring recovery sweep owns the unfinished delivery.
If a create request omits client_message_id, an inline deployment cannot offer a duplicate-safe retry. It returns 202 Accepted with delivery_pending: true and the committed message; clients must treat that message as sent and must not repeat the create request. A committed deletion whose final realtime delivery is incomplete also returns this success/pending response rather than create-specific retry guidance.
New API-created messages opt into delivery recovery. When Solid Queue is enabled, MessageDeliveryRecoveryJob sweeps up to 100 of the least-recently-attempted incomplete deliveries every minute. This bounded rotation prevents one failing message from blocking newer recovery work. Recovery does not expire incomplete delivery because there is not yet an operator-owned dead-letter queue; each failed attempt is logged and rotated behind untouched work. A recovered notification stage may enqueue another push job, while per-provider forwarding is checkpointed by notification ID so duplicate jobs do not duplicate provider attempts. Messages created before tracking shipped are excluded so deployment cannot replay historical notifications or broadcasts. Deployments using the low-volume inline adapter retain synchronous delivery and identical-request replay, but do not run recurring jobs.
Posting a message creates in-app message notifications for other visible channel recipients and can enqueue Web Push delivery when push is configured.
The API mounts ActionCable at /cable. The web client first exchanges its normal API auth for a short-lived cable token:
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/v1/cable_token |
Any signed-in user | Issue a short-lived, single-use ActionCable token |
Then it connects to /cable?token=... and subscribes to:
{
"channel": "ChannelMessagesChannel",
"channel_id": 12
}The server authorizes the subscription against the same channel visibility rules as the REST API. Broadcast payloads look like:
{
"event": "created",
"channel_id": 12,
"message": {
"id": 44,
"channel_id": 12,
"body": "Can someone share the Zoom link?",
"edited_at": null,
"deleted_at": null,
"created_at": "2026-04-20T10:00:00Z",
"updated_at": "2026-04-20T10:00:00Z",
"author": {
"id": 2,
"full_name": "Student One",
"email": "student@example.com",
"role": "student",
"avatar_url": null
}
}
}event may be created, updated, or deleted. The REST polling/refetch path remains as a fallback for reconnects and stale tabs.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/cohorts |
Staff | List all cohorts |
GET |
/api/v1/cohorts/:id |
Staff | Show cohort with enrollments |
GET |
/api/v1/cohorts/:id/student_view |
Staff | Read-only preview of the cohort's student experience |
POST |
/api/v1/cohorts |
Admin | Create cohort |
PATCH |
/api/v1/cohorts/:id |
Admin | Update cohort |
DELETE |
/api/v1/cohorts/:id |
Admin | Delete cohort |
PATCH |
/api/v1/cohorts/:id/module_access |
Admin | Update module access settings |
PATCH |
/api/v1/cohorts/:id/announcements |
Admin | Legacy JSON cohort notices; use /announcements for Phase 4 communication |
PATCH |
/api/v1/cohorts/:id/recordings |
Admin | Update legacy cohort recordings list |
PATCH |
/api/v1/cohorts/:id/class_resources |
Admin | Update cohort resources |
| Method | Path | Auth | Description |
|---|---|---|---|
PATCH |
/api/v1/cohorts/:cohort_id/modules/:module_id/submission_windows |
Staff | Atomically set or clear weekly close times |
Close times use ISO 8601 instants and must include Z or a numeric UTC offset. A null close time clears that week's window. Week numbers must exist in the selected module.
{
"submission_windows": [
{ "week_number": 1, "submissions_close_at": "2026-07-18T08:00:00Z" },
{ "week_number": 2, "submissions_close_at": null }
]
}Closed windows prevent student submissions, resubmissions, redo updates, manual work completion, and GitHub sync for that week. Staff actions, lesson reading, and video progress remain available.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/cohorts/:cohort_id/office_hours |
Enrolled student or Staff | List active live-class/office-hour definitions and upcoming occurrences |
POST |
/api/v1/cohorts/:cohort_id/office_hours |
Staff | Create a one-time or weekly live class or office-hours session |
PATCH |
/api/v1/cohorts/:cohort_id/office_hours/:id |
Staff | Update a session |
DELETE |
/api/v1/cohorts/:cohort_id/office_hours/:id |
Staff | Delete a session |
Offset-bearing ISO 8601 values are treated as absolute instants. Values from datetime-local inputs are interpreted as wall-clock times in the supplied IANA timezone. Nonexistent or ambiguous daylight-saving wall times are rejected.
{
"title": "Instructor Office Hours",
"description": "Bring questions from the week.",
"starts_at": "2026-07-18T18:00",
"ends_at": "2026-07-18T19:00",
"meeting_url": "https://meet.example.com/csg",
"timezone": "Pacific/Guam",
"recurrence": "weekly",
"event_kind": "office_hours",
"active": true
}event_kind is live_class or office_hours; existing records default to office_hours.
All endpoints in this section are staff-only. Intervention notes and recovery check-ins are never included in student payloads.
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/interventions |
List cases; filters: enrollment_id, owner_id, status, or due=true |
POST |
/api/v1/interventions |
Create one owned case for an enrollment and active trigger |
GET |
/api/v1/interventions/:id |
Show case, private notes, connected enrollment, help request, and recovery-plan ID |
PATCH |
/api/v1/interventions/:id |
Update state, owner, action, severity, follow-up, outcome, or resolution |
POST |
/api/v1/interventions/:id/notes |
Append an immutable staff-only note |
GET |
/api/v1/recovery_plans |
List plans; filters: enrollment_id, status, or due=true |
POST |
/api/v1/recovery_plans |
Create an extended-absence or manual recovery plan |
GET |
/api/v1/recovery_plans/:id |
Show plan and private check-in history |
PATCH |
/api/v1/recovery_plans/:id |
Update pace, scope, cadence, next check-in, status, or outcome |
POST |
/api/v1/recovery_plans/:id/check_ins |
Append a check-in and schedule the next one |
The server builds evidence_snapshot; client-provided evidence is ignored. Snapshots contain only source record IDs, categories, counts, and timestamps. Message bodies, code, submission text, and private feedback are prohibited. Active cases require an owner and follow-up date. Resolved cases require a categorical outcome and resolution summary.
The daily InterventionFollowUpJob creates one owner notification per due date. Changing the follow-up date resets that notification claim. Resolving or canceling the case closes its notifications.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/cohorts/:cohort_id/enrollments |
Staff | List enrollments in cohort |
POST |
/api/v1/cohorts/:cohort_id/enrollments |
Staff | Enroll user in cohort |
GET |
/api/v1/enrollments/:id |
Staff | Show enrollment details |
PATCH |
/api/v1/enrollments/:id |
Staff | Update enrollment status |
DELETE |
/api/v1/enrollments/:id |
Staff | Remove enrollment |
POST |
/api/v1/enrollments/:id/restart |
Admin | Audit and clear scoped learning state; requires exact email confirmation and atomically creates an owned intervention, weekly follow-up, and recovery plan |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/enrollments/:enrollment_id/module_assignments |
Staff | List module assignments |
POST |
/api/v1/enrollments/:enrollment_id/module_assignments |
Staff | Create assignment |
GET |
/api/v1/module_assignments/:id |
Staff | Show assignment |
PATCH |
/api/v1/module_assignments/:id |
Staff | Update (unlock, set date override) |
DELETE |
/api/v1/module_assignments/:id |
Staff | Delete assignment |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/enrollments/:enrollment_id/lesson_assignments |
Staff | List lesson assignments |
POST |
/api/v1/enrollments/:enrollment_id/lesson_assignments |
Staff | Create assignment |
GET |
/api/v1/lesson_assignments/:id |
Staff | Show assignment |
PATCH |
/api/v1/lesson_assignments/:id |
Staff | Update (unlock, set date override) |
DELETE |
/api/v1/lesson_assignments/:id |
Staff | Delete assignment |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/progress |
Authenticated | Current user's progress records |
PATCH |
/api/v1/progress |
Authenticated | Mark a content block as completed |
GET |
/api/v1/progress/student/:user_id |
Staff | View a student's active enrollment progress, or an exact current/historical enrollment with cohort_id |
The staff student response keeps operational fields scoped to the selected enrollment and includes learning_evidence_scope. Progress and submissions are intentionally keyed to the learner and curriculum content, so the scope reports the curriculum and whether evidence is shared across multiple same-curriculum enrollments.
Update body:
{
"content_block_id": 42,
"status": "completed"
}| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/submissions |
Staff | List submissions (filterable) |
GET |
/api/v1/submissions/:id |
Authenticated | Show submission, including lesson_id, module_id, and module_name relationship fields |
POST |
/api/v1/submissions |
Authenticated | Create submission |
PATCH |
/api/v1/submissions/:id |
Staff | Update submission |
PATCH |
/api/v1/submissions/:id/grade |
Staff | Grade a submission |
GET |
/api/v1/submissions/:id/github_issue |
Staff | Get linked GitHub issue |
GET |
/api/v1/submissions/:id/github_checks |
Staff or submission owner | Read persisted check-run metadata for the submission's current commit |
POST |
/api/v1/submissions/:id/github_checks |
Staff | Refresh current-commit check metadata from GitHub |
Grade body:
{
"grade": "A",
"feedback": "Great work!",
"base_submission_updated_at": "2026-09-06T10:44:12.123456Z"
}base_submission_updated_at is required and must be the exact updated_at value returned with the submission. The API returns 409 Conflict with code stale_submission if the record changed before the grade was saved.
Grade values: A (0), B (1), C (2), R (3 — redo required)
Creating a submission enqueues background creation of an in-app submission notification for non-archived instructors and admins, whose access is currently platform-wide. Grading enqueues the corresponding student notification. Jobs carry the originating event timestamp so stale or duplicate execution neither re-enqueues push nor resets an already-read notification; a later regrade remains a distinct event. Expo delivery deep-links staff to /staff/submission/:id and students to /lesson/:lesson_id; Web Push opens the equivalent web destination. Web and Expo fanout are isolated from one another. The existing message/email preference is not reused as a global opt-out for announcements or learning alerts.
GitHub check refresh requires GITHUB_ORGANIZATION_ADMIN_TOKEN and a saved GitHub repository URL plus commit SHA. The platform persists check names, lifecycle state, conclusion, app/workflow labels, timestamps, and HTTPS detail links. It intentionally does not persist check output, annotations, or log bodies. Old-commit records remain available for audit but are excluded from the current submission projection.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/cohorts/:cohort_id/learning_insights |
Staff | Explainable objective evidence and curriculum revision/retry patterns for a cohort |
An optional user_id filters the projection to one learner and must identify an enrollment in the selected cohort. Evidence comes only from rubric criterion results, graded objective-aligned submissions, and the latest attempt for objective-linked knowledge checks. Completion, watch time, messages, student work/code, and private feedback are excluded. Every returned signal includes the IDs needed to open its learner, content, or exact submission source. The response also states its evidence rule and the guarantee that it never changes grades, access, or progress automatically.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/cohorts/:id/modules/:module_id/submissions |
Staff | All submissions for a module in a cohort |
POST |
/api/v1/cohorts/:id/modules/:module_id/sync_github |
Staff | Sync all students' GitHub repos |
POST |
/api/v1/cohorts/:id/modules/:module_id/sync_github/:user_id |
Staff | Sync one student's GitHub repo |
All errors follow a consistent format:
{
"error": "Not authorized"
}Common HTTP status codes:
401— Missing or invalid JWT403— Insufficient role permissions404— Resource not found422— Validation errors