iblai/api

canvas-course-builder

Build and populate Canvas LMS courses through the Canvas REST API — creating course shells, modules and module items, pages, assignments, quizzes (Classic and New Quizzes), discussions, announcements, files, assignment groups, sections and enrollments, plus bulk seeding from a spec file, course copies and Common Cartridge imports. Use this skill whenever the user mentions Canvas, Instructure, canvas.instructure.com, a Canvas API token, `/api/v1/courses`, or asks to script, seed, migrate, bulk-c…

First seen Aug 21, 2026

Installation

$ npx skills add iblai/api --skill canvas-course-builder

Summary

  • Build and populate Canvas LMS courses through the Canvas REST API — creating course shells, modules and module items, pages, assignments, quizzes (Classic and New Quizzes), discussions, announcements, files, assignment groups, sections and enrollments, plus bulk seeding from a spec file, course copies and Common Cartridge imports.
  • Use this skill whenever the user mentions Canvas, Instructure, canvas.instructure.com, a Canvas API token, `/api/v1/courses`, or asks to script, seed, migrate, bulk-create, or automate anything in an LMS course — including one-off scripts, sandbox/demo course generation, syllabus or module scaffolding, roster loading, and debugging Canvas API errors like 401/403/404, "Rate Limit Exceeded", unpublished content, or duplicated objects on re-runs.
  • Reach for it even when the user only says "our LMS" or names a course by SIS ID and never says the words "Canvas API".

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from iblai/api · top by installs.

npx skills add iblai/api

Browse all from iblai/api

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 15
License LICENSE
Default branch main
Open issues 1
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 11,923 B
  • docs SUMMARY.md 932 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

Building Canvas courses via the API

Canvas has no "create a whole course" endpoint. A course is assembled from a dozen independent resources, each with its own publish state, and the API will happily let you build something that looks complete in the admin view and is entirely invisible to students. Most of the difficulty in this task is ordering, publish state, and idempotency — not the individual HTTP calls.

Before touching the API

Three things to establish first, because getting them wrong is expensive:

  1. Which instance and whose token. Read CANVASAPIURL (e.g.

https://school.instructure.com) and CANVASAPITOKEN from the environment. Never put a token in a file you write, in a command line that lands in shell history, or in anything you show back to the user. If they paste a token into the conversation, use it from an env var you set and mention that they should rotate it afterwards.

  1. Confirm identity and permissions before writing anything. GET /api/v1/users/self

tells you who the token belongs to; GET /api/v1/accounts tells you which accounts they can create courses in (an empty list means teacher-level access — they can populate existing courses but not create new ones, which changes the whole plan). Do this even when the user seems certain, because a token scoped to the wrong sub-account fails halfway through a build and leaves debris.

  1. Is this production? Creating course content is not reversible in a satisfying way —

deleted objects linger, notifications fire, students see things. Ask which course or sub-account to build in, and prefer a sandbox sub-account or a test course for the first run. If the user is clearly iterating on a script, suggest they point it at a throwaway course first.

The build order that works

Dependencies run one direction. Follow this and you never have to backfill an ID:

1. Course shell            POST /accounts/:id/courses            (leave unpublished)
2. Sections                POST /courses/:id/sections
3. Assignment groups       POST /courses/:id/assignment_groups   (assignments need these)
4. Files                   3-step upload (see references/recipes.md)
5. Pages                   POST /courses/:id/pages
   Assignments             POST /courses/:id/assignments
   Discussions             POST /courses/:id/discussion_topics
   Quizzes + questions     POST /courses/:id/quizzes then .../questions
6. Modules                 POST /courses/:id/modules
7. Module items            POST /courses/:id/modules/:mid/items  (needs IDs from step 5)
8. Front page / syllabus   PUT  /courses/:id/front_page, PUT /courses/:id
9. Enrollments             POST /courses/:id/enrollments
10. Publish everything     modules → then the course itself

Steps 5 and 6 can run concurrently in principle; don't. Canvas throttles on concurrency (see below) and serial execution is barely slower in practice.

Publish last, and publish deliberately. Content objects, modules, module items, and the course each carry an independent published flag. A published assignment inside an unpublished module is invisible. Publishing the course while enrollments exist sends notification emails to real people. Build the whole thing unpublished, verify it, then publish modules and finally the course with PUT /courses/:id and course[event]=offer.

A page inside a module is referenced by pageurl, not by ID. This is the single most common failure in module wiring. When you create a page Canvas returns a url slug derived from the title; capture it. Every other content type uses contentid. SubHeader and ExternalUrl items need neither.

Idempotency: assume the script will be run twice

Canvas has no upsert. A second run of a naive script produces a second copy of every page, assignment and module, and the user will not notice until a student does. Two workable approaches:

  • Manifest file (preferred for scripted builds): write the created object IDs to a JSON

file keyed by a stable name from the spec. On re-run, if the key exists, PUT instead of POST. scripts/build_course.py does this.

  • Match by title (for ad-hoc work): list existing objects with GET and reuse anything

whose title matches before creating. Slower and fragile against renames, but needs no state.

Say which one you used, and where the manifest lives, so re-runs are predictable.

Request mechanics that bite

Parameter encoding. Canvas accepts both form-encoded bracket notation (assignment[submissiontypes][]=onlineupload) and a JSON body with Content-Type: application/json. Form encoding is what the docs show and what every endpoint accepts. But anything containing a list of objects — quiz answers, assignment overrides, module overrides — should go as a JSON body, because bracket-encoded arrays of hashes (answers[][text]) rely on Rails' positional grouping and silently mangle answers that share key sets. scripts/canvasclient.py handles both; use asjson=True for those.

Course dates need a flag to exist. course[startat] and course[endat] are silently discarded unless course[restrictenrollmentstocoursedates] is true. Canvas returns 200 and a Course object with null dates. Since assignment availability is computed against course and term dates, this shows up much later as "why can't students see the assignment I set a due date on". Set the flag whenever you set dates.

A new course may not be empty. If the account has a course template configured, Canvas copies it into every new course. Pass top-level skipcoursetemplate=true when you want a clean shell to build into, or your build lands on top of someone else's content.

Four create params are not under course[...]. offer, enrollme, skipcoursetemplate and enablesis_reactivation are top-level. Nesting them as course[offer] is accepted and ignored — the course just doesn't publish.

Booleans and nulls. Send true/false as lowercase strings in form encoding. Omitting a key leaves the existing value; sending an empty string usually clears it. There is no way to distinguish "unset" from "set to empty" in form encoding, which is why updates should send only the fields you intend to change.

Pagination. Collection endpoints return 10 items by default and paginate via the Link header with rel="next" — there is no pagecount in the body. Set perpage=100 (the practical maximum) and follow next until it's absent. Do not construct page URLs by hand; some endpoints use bookmark cursors rather than page numbers.

Throttling. Canvas uses a leaky bucket keyed on the access token, not the user or account. Every response carries X-Request-Cost; throttled requests come back as 403 (or 429 on newer builds) with "Rate Limit Exceeded" in the body, which is otherwise indistinguishable from a permissions 403 — check the body text. Sequential requests are almost never throttled; parallel requests take an extra up-front penalty and are the usual cause. If you hit it, back off exponentially and reduce concurrency to one.

SIS IDs. Anywhere Canvas takes an ID you can pass siscourseid:BIO101-F26, sisuserid:0001234, sissectionid:.... URL-encode the colon-suffixed value. This is much safer than hardcoding numeric IDs that differ between test and production instances.

Rich text. body, description, message and syllabus_body are HTML, sanitized server-side. Relative links to other course objects break on course copy; use Canvas's /courses/:id/pages/slug form or, better, wire content together with modules instead of inline links.

When not to build object-by-object

If the user is duplicating an existing course, copying between instances, or importing from another LMS, the Content Migrations API does in one call what would otherwise be hundreds: POST /courses/:id/contentmigrations with migrationtype=coursecopyimporter (or commoncartridgeimporter with an uploaded .imscc). It preserves internal links and dates, which hand-built copies do not. See references/recipes.md. Say so if the user is about to hand-roll something a migration would handle.

For roster loading at scale, SIS Imports (POST /accounts/:id/sis_imports with a CSV) beat per-user enrollment calls by orders of magnitude — but they require account-admin rights and can deactivate enrollments not present in the file, so only reach for them when the user actually owns the SIS integration.

Bundled tooling

scripts/canvas_client.py — a small CanvasClient covering auth, pagination, the bracket/JSON encoding split, throttle-aware retry, and the three-step file upload. Import it rather than rewriting requests boilerplate; the retry and pagination logic in particular is easy to get subtly wrong.

scripts/build_course.py — takes a YAML or JSON course spec and builds the whole course in dependency order, idempotently, with --dry-run. Use it when the user wants a repeatable build; use the client directly for one-off surgery.

export CANVAS_API_URL=https://school.instructure.com CANVAS_API_TOKEN=...
python scripts/build_course.py course.yaml --account 1 --dry-run
python scripts/build_course.py course.yaml --account 1        # then for real
python scripts/build_course.py course.yaml --course 12345     # populate an existing course

Both need requests; build_course.py also wants pyyaml for YAML specs (JSON works without it).

assets/course_spec.example.yaml is a fully-commented spec showing every supported content type. When a user describes a course in prose, translating it into this spec and running the builder is usually faster and more reviewable than writing a bespoke script — and the spec is something they can edit and re-run.

Reference material

Read these when you need exact parameters rather than the shape of the work:

  • references/endpoints.md — endpoint and parameter cheat sheet for every resource above,

including the exact module_item[...] and course[...] field names.

  • references/content-types.md — request bodies for pages, assignments, discussions,

Classic quizzes (with all question types and answer formats) and New Quizzes, which use a completely different API at /api/quiz/v1/.

  • references/recipes.md — file uploads, enrollments and sections, course copy and Common

Cartridge migrations, publishing, and a table of error responses with what actually causes each one.

Reporting back

End with what exists now, not what was attempted: the course ID and URL, counts per content type, publish state of the course and modules, anything that failed, and the manifest path. If the course is still unpublished — which it should be by default — say so explicitly and give the one-line command to publish it. Users routinely assume a successful build means a live course.