Install
openclaw skills install @chrischall/canvas-parent-apiQuery Canvas LMS (Instructure) from a shell with curl and a bearer access token instead of running the canvas-parent-mcp server — courses, grades, assignments, submissions, calendar, planner, announcements, conversations, discussions, and files for yourself or a linked observee. Use when you want Canvas data without the MCP, in a script, or on a machine where the MCP isn't installed.
openclaw skills install @chrischall/canvas-parent-apiCanvas's REST API (/api/v1/...) is a normal per-institution API reachable
directly with curl — no browser bridge, no bot wall. Every request needs an
Authorization: Bearer <token> header; there is no anonymous access. The
host is per-institution (https://<district>.instructure.com, or a
self-hosted domain) — always read it from $CANVAS_BASE_URL, never hardcode
one tenant.
This is the same data the canvas_* MCP tools return, reached with plain
curl instead of a running server.
Pick whichever your institution allows (mirrors canvas-parent-mcp's
CANVAS_TOKEN / OAuth modes — the session-cookie and fetchproxy modes need
the MCP or a browser and are out of scope here):
A. Personal access token (simplest, if allowed)
Canvas web UI → Account → Settings → + New Access Token. Most institutions disable this for non-admins, so try B if it's not offered.
export CANVAS_BASE_URL=https://<district>.instructure.com # per-institution, no trailing slash
export CANVAS_TOKEN=<token from the UI>
B. OAuth via the mobile QR-login flow
In the Canvas mobile app: Account → QR for Login (or "Pair with
Observer/QR Login"), scan it with any camera to get the URL it encodes
(https://sso.canvaslms.com/canvas/login?domain=...&code=...). Exchange it
once for OAuth credentials with the helper this same repo ships (no MCP
server needs to be running — it's a one-off CLI).
It prints four NAME=value lines to stdout
(CANVAS_BASE_URL/CANVAS_CLIENT_ID/CANVAS_CLIENT_SECRET/CANVAS_REFRESH_TOKEN)
— export them into the shell, since the next curl reads them as env
vars and this step can't be skipped:
eval "$(npx canvas-parent-mcp-qr-login "<qr-url>" | sed 's/^/export /')"
Then mint (and later re-mint) a short-lived access token from the refresh
token with curl directly — no need to re-scan the QR each time:
curl -s -X POST "$CANVAS_BASE_URL/login/oauth2/token" \
-d grant_type=refresh_token \
-d client_id="$CANVAS_CLIENT_ID" \
-d client_secret="$CANVAS_CLIENT_SECRET" \
-d refresh_token="$CANVAS_REFRESH_TOKEN" \
| jq -r '.access_token'
Export the result as CANVAS_TOKEN (access tokens expire in ~1h — re-run
this when calls start 401ing).
curl -s \
-H "Authorization: Bearer $CANVAS_TOKEN" \
-H "Accept: application/json+canvas-string-ids, application/json" \
"$CANVAS_BASE_URL/api/v1/users/self/profile" \
| sed 's/^while(1);//' | jq .
Two quirks every call needs:
Accept: application/json+canvas-string-ids, application/json — without
it, large numeric IDs can silently round-trip wrong (JS 2^53 overflow).while(1); to the JSON body.
Strip it before piping to jq (sed 's/^while(1);//'), or it'll fail to
parse.Most detail/list-by-course endpoints need a courseId and (for parents) an
observee's user ID — get those first, then substitute them into the paths in
references/canvas-endpoints.md:
# Your own profile (sanity-check the token works)
curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
"$CANVAS_BASE_URL/api/v1/users/self/profile" | sed 's/^while(1);//' | jq '{id, name, primary_email}'
# Students linked to your observer account (empty array for a plain student token)
curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
"$CANVAS_BASE_URL/api/v1/users/self/observees?include[]=avatar_url" | sed 's/^while(1);//' | jq '.[] | {id, name}'
# Your (or an observee's) active courses, with grades
curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
"$CANVAS_BASE_URL/api/v1/users/self/courses?enrollment_state=active&state[]=available&include[]=total_scores&include[]=current_grading_period_scores&include[]=term" \
| sed 's/^while(1);//' | jq '.[] | {id, name}'
For an observee, swap users/self for users/<observeeId> in any path that
starts that way (per-user endpoints); course-scoped endpoints
(/api/v1/courses/<courseId>/...) don't need the observee ID at all — Canvas
scopes them to whichever user the token belongs to.
Canvas paginates with an RFC 5988 Link header, not a body field. Fetch
headers separately to follow rel="next":
url="$CANVAS_BASE_URL/api/v1/courses/123/students/submissions?student_ids[]=self&per_page=100"
while [ -n "$url" ]; do
headers=$(curl -sD - -o page.json -H "Authorization: Bearer $CANVAS_TOKEN" \
-H "Accept: application/json+canvas-string-ids, application/json" "$url")
sed 's/^while(1);//' page.json | jq -c '.[]' >> all.jsonl
url=$(echo "$headers" | grep -i '^link:' | grep -oE '<[^>]+>; rel="next"' | grep -oE 'https?://[^>]+')
done
Bumping per_page (Canvas allows up to 100) is usually enough for one-off
queries; only bother with the loop for genuinely large collections.
refresh_token
exchange above; the refresh token itself is long-lived.All 18 endpoints (courses, assignments, submissions, grades, calendar,
planner, announcements, conversations, discussions, files) are in
references/canvas-endpoints.md with the real query params and a jq
projection for each.