Last updated

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

  1. Preflight: node .claude/skills/iblai-api-studio-auth/scripts/studio-login.mjs --check prints two oks (the second one is the LMS session used here); otherwise run /iblai-api-studio-auth first — do not attempt the calls below.
  2. The course is published (/iblai-api-studio-publish) and its enrollment_start is in the past and before start_date (/iblai-api-studio-settings) — otherwise enrollment and outline reads legitimately return "not yet".
  3. A course key.

Auth & conventions

  • Base URL: $LMS_URL (https://learn.iblai.app; test https://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_USERNAME is 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; 404 when 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-settings fields plus display_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 with null values for a freshly built course (cache lag) — treat course_metadata above as the source of truth and use this only for pacing/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=all for 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 (mode defaults to audit) → the enrollment object. Idempotent. { "course_details": {…}, "is_active": false } unenrolls yourself. Passing "user": "<other>" needs global staff (404 otherwise) — 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=unenroll removes. identifiers is comma/newline separated; unknown emails get user: false (with auto_enroll=true they are enrolled when they register). email_students=true sends 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-create is 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 in course_metadata and the catalog without a separate sync. Api-Token catalog operations (programs, pathways, bulk enrollment, metadata for other orgs) are /iblai-api-catalog and /iblai-api-catalog-invitation.
  • "Not visible" checklist: enroll_status.is_enrolled → the unit present in course_outline (published + released) → courses/v1/courses start in the past → hidden/invitation_only false.
  • 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_enrollment is form-encoded, so the example drops the JSON content type ("${L[@]:0:11}" keeps cookies, CSRF, Origin, Referer, Accept).

Copyright © ibl.ai | support@iblai.zendesk.com