iblai-api-studio-lms
Mirrored from
iblai/vibe·skills/content/iblai-api-studio-lms/SKILL.md. This page is generated — edit it in the repository, not here.
The LMS app (lms.ibl.ai; test lms.iblai.org) renders a course from the
LMS API on $LMS_URL plus the ibl.ai catalog. This skill uses the LMS session
captured by /iblai-api-studio-auth to make the same calls, so you can prove a
build is visible, enroll users, and read what they will see. Nothing here
edits course content.
Before you start
- Preflight:
node .claude/skills/iblai-api-studio-auth/scripts/studio-login.mjs --checkprints twooks (the second one is the LMS session used here); otherwise run/iblai-api-studio-authfirst — do not attempt the calls below. - The course is published (
/iblai-api-studio-publish) and itsenrollment_startis in the past and beforestart_date(/iblai-api-studio-settings) — otherwise enrollment and outline reads legitimately return "not yet". - A course key.
Auth & conventions
- Base URL:
$LMS_URL(https://learn.iblai.app; testhttps://learn.iblai.org). LMS app:$LMS_APP_URL. - Auth: LMS session cookies from
studio.env(values single-quoted — strip them in any non-bash parser:/iblai-api-studio-auth). - Snippet:
set -a; . ./studio.env; set +a L=(-s -b "sessionid=$LMS_SESSION; csrftoken=$LMS_CSRF" -H "X-CSRFToken: $LMS_CSRF" \ -H "Origin: $LMS_URL" -H "Referer: $LMS_URL/" -H "Accept: application/json" -H "Content-Type: application/json") COURSE="course-v1:<org>+<number>+<run>";CE= the same URL-encoded (needed in every query string:%3A%2B).$STUDIO_USERNAMEis the signed-in user; the same person must be enrolled to read blocks "as a user".- Enrolling/unenrolling other people is outward-facing — confirm with the user first.
Reads
- GET
/api/user/v1/me→{ "username" }— session check. - GET
/api/ibl/enrollment/enroll_status?course_id={CE}→{ "is_enrolled", "can_enroll", "invitation_only", "is_admin" }for the signed-in user. - GET
/api/enrollment/v1/enrollment/{username},{COURSE}→{ "created", "mode": "audit", "is_active", "course_details": { "course_id", "course_name", "enrollment_start", "enrollment_end", "course_start", "course_end", "invite_only", "course_modes": […], "pacing_type" }, "user" }(own enrollment;404when not enrolled). - GET
/api/ibl/v1/course_metadata?course_key={CE}— the merged object the LMS app shows on the course page: all/iblai-api-studio-settingsfields plusdisplay_name,course_price,advertised_start,display_organization,certificate_available,course_outline(published tree). Catalog fields (subject,tags,level, …) read back from here prove the catalog sync. - GET
/api/courses/v1/courses/{COURSE}→ Open edX course detail:{ "id", "name", "org", "number", "start", "end", "enrollment_start", "enrollment_end", "pacing": "instructor" | "self", "short_description", "overview", "effort", "media": { "course_image": { "uri" }, "banner_image", "course_video": { "uri": "http://www.youtube.com/watch?v=…" } }, "blocks_url", "hidden", "invitation_only" }. On some deployments this returns the shape withnullvalues for a freshly built course (cache lag) — treatcourse_metadataabove as the source of truth and use this only forpacing/hidden. - GET
/api/ibl/completion/course_outline/{COURSE}?course_id={CE}— the published outline as a user sees it, with completion: nested{ id, block_id, type, display_name, lms_web_url, student_view_url, graded, start, has_score, effort_time, children: […] }down to units. A unit missing here is unpublished or not yet released. - GET
/api/courses/v2/blocks/?course_id={CE}&username={user}&depth=all&all_blocks=true&requested_fields=children,graded,format,due,type→{ "root", "blocks": { "<usage key>": { "id", "type", "display_name", "lms_web_url", "student_view_url", "children": […] } } }— every visible block incl. components.GET /api/courses/v2/blocks/{usage_key}?username={user}&depth=allfor one unit. - GET
/api/course_home/progress/{COURSE}→{ "course_grade": { "percent", "letter_grade", "is_passing" }, "grading_policy": { "assignment_policies": [ { "type", "weight", "num_total", "num_droppable", "short_label" } ], "grade_range" }, "section_scores": [ { "display_name", "subsections": [ { "display_name", "assignment_type", "num_points_earned", "num_points_possible", "problem_scores", "url" } ] } ], "completion_summary", "enrollment_mode", "certificate_data" }— grading policy and structure as graded. - GET
/api/ibl/users/manage/roles/?username={user}→[ { "role", "org", "course" } ](course-staff / course-instructor / org-instructor …). - POST
/courses/{COURSE}/instructor/api/get_students_features(form-encoded, empty body; course staff only) →{ "course_id", "students": [ { "id", "username", "email", "name", "enrollment_mode", … } ] }— the enrolled users.
Writes
- POST
/api/enrollment/v1/enrollment{ "course_details": { "course_id": "<COURSE>" } }— enroll yourself (modedefaults toaudit) → the enrollment object. Idempotent.{ "course_details": {…}, "is_active": false }unenrolls yourself. Passing"user": "<other>"needs global staff (404otherwise) — use the instructor route below. - POST
/courses/{COURSE}/instructor/api/students_update_enrollment(form-encoded; course staff/instructor) — enroll or unenroll other users by email or username. Confirm with the user first.
→curl "${L[@]:0:11}" -X POST "$LMS_URL/courses/$COURSE/instructor/api/students_update_enrollment" \ --data-urlencode "identifiers=jdoe@example.com,asmith" --data-urlencode "action=enroll" \ --data-urlencode "auto_enroll=true" --data-urlencode "email_students=false"{ "action": "enroll", "results": [ { "identifier", "before": { "user", "enrollment", "allowed", "auto_enroll" }, "after": {…} } ], "auto_enroll": true }.action=unenrollremoves.identifiersis comma/newline separated; unknown emails getuser: false(withauto_enroll=truethey are enrolled when they register).email_students=truesends the LMS enrollment email.
Example
set -a; . ./studio.env; set +a
L=(-s -b "sessionid=$LMS_SESSION; csrftoken=$LMS_CSRF" -H "X-CSRFToken: $LMS_CSRF" \
-H "Origin: $LMS_URL" -H "Referer: $LMS_URL/" -H "Accept: application/json" -H "Content-Type: application/json")
COURSE="course-v1:acme+DATA101+2026-T1"; CE=$(python3 -c "import urllib.parse;print(urllib.parse.quote('$COURSE',safe=''))")
curl "${L[@]}" -X POST "$LMS_URL/api/enrollment/v1/enrollment" -d "{\"course_details\":{\"course_id\":\"$COURSE\"}}" >/dev/null
curl "${L[@]}" "$LMS_URL/api/ibl/enrollment/enroll_status?course_id=$CE" # {"is_enrolled":true,…}
curl "${L[@]}" "$LMS_URL/api/ibl/completion/course_outline/$COURSE?course_id=$CE" | python3 -c '
import sys,json
def walk(n,d=0):
print(" "*d + n["type"].ljust(11), n["display_name"])
for c in n.get("children",[]): walk(c,d+1)
walk(json.load(sys.stdin))'
ORG=${COURSE#course-v1:}; ORG=${ORG%%+*}
echo "LMS app URL: $LMS_APP_URL/platform/$ORG/courses/$COURSE"
Notes
- Catalog visibility: a course created with
/iblai-api-studio-course-createis registered in the ibl.ai catalog for its org at creation and appears in the LMS app's catalog immediately; settings written through/iblai-api-studio-settings(subject, tags, level, …) show up incourse_metadataand the catalog without a separate sync. Api-Token catalog operations (programs, pathways, bulk enrollment, metadata for other orgs) are/iblai-api-catalogand/iblai-api-catalog-invitation. - "Not visible" checklist:
enroll_status.is_enrolled→ the unit present incourse_outline(published + released) →courses/v1/coursesstartin the past →hidden/invitation_onlyfalse. - The LMS app's course page is
$LMS_APP_URL/platform/<org>/courses/<course_key>; courseware is…/course-content/<course_key>/course(add?unit_id=<unit locator>to deep link). - The LMS app itself authenticates with a JWT (
Authorization: JWT …) to the same endpoints; the session cookie is equivalent for these reads and lasts longer. students_update_enrollmentis form-encoded, so the example drops the JSON content type ("${L[@]:0:11}"keeps cookies, CSRF, Origin, Referer, Accept).