UPSked catalog API
Public REST API v1
Read-only access to semesters, course search, and sections. Same data the planner uses, with a stable snake_case JSON contract. Use the navigation for each endpoint, then run real requests in Try it.
Authentication
Every request needs a personal API key in the Authorization header (not query strings — avoids leaking keys in logs and referrers).
Authorization: Bearer upsked_api_v1_...
Create keys from your UPSked account (same session as the planner). Keys are scoped to catalog:read by default.
Conventions
- Base URL (production):
https://api.upsked.com— paths/semesters,/courses,/sections(no/api/public/v1prefix on that host). - Same deployment: your app origin +
/api/public/v1/semesters(and/courses,/sections). Handy on localhost orwww.upsked.com. - Successful lists return
{ "object": "list", "type": "…", … }. - Section identity in public JSON is
class_code(CRS), not internal UUIDs.
Rate limits
Catalog routes have dedicated per-IP and per-account limits. If you hit 429, slow down, use pagination, and reuse ETag on /sections when possible.
{
"object": "error",
"code": "rate_limited",
"message": "Too many requests. Use smaller pages (limit/offset) and cache responses."
}Pagination
/courses and /sections return a pagination object with has_more, next_offset, and related hints. On /sections, limit/offset count options (bundles count as one row). Offset-based (not cursor) for v1 — use next_offset from the previous response when has_more is true.
HTTP
GET /semesters
Returns all semesters and a default_semester_id hint.
curl -sS 'https://api.upsked.com/semesters' \ -H 'Authorization: Bearer upsked_api_v1_YOUR_KEY'
HTTP
GET /courses
| Parameter | Required | Description |
|---|---|---|
| query | yes | Search string (course id / title). |
| semester_id | yes | 6-digit semester id. |
| limit | no | 1–20, default 8. |
| offset | no | 0–400, default 0. |
Course rows are metadata only (section_count, no schedules). Use GET /sections for schedulable options.
curl -g -sS 'https://api.upsked.com/courses?query=math&semester_id=120252&limit=8&offset=0' \ -H 'Authorization: Bearer upsked_api_v1_YOUR_KEY'
HTTP
GET /sections
Send exactly one of course_id or comma-separated class_code (max 24 codes). Each list item is a schedulable option (standalone section, block, or atomic bundle); sections inside an option lists every part you must take together.
| Parameter | Required | Description |
|---|---|---|
| semester_id | yes | 6-digit semester id. |
| course_id | one of | e.g. EEE 141 (URL-encoded). |
| class_code | one of | Comma-separated CRS codes. |
| limit | no | 1–20, default 20. |
| offset | no | 0–500, default 0 (pages over options, not flat section rows). |
Responses include ETag. Send If-None-Match to get 304 when nothing changed (respects limit/offset). Seat counts (slots_available/slots_total) are omitted unless the deployment sets PUBLIC_API_SHOW_SECTION_SLOTS=true.
curl -g -sS 'https://api.upsked.com/sections?semester_id=120252&course_id=MATH%2021&limit=20&offset=0' \ -H 'Authorization: Bearer upsked_api_v1_YOUR_KEY'
Errors
Every error returns object: "error", a stable code, and a human message. 401 missing/invalid key, 403 missing catalog:read, 400 bad query (often with errors[] per param), 404 unknown course_id or no matching class_code rows, 429 rate limit, 500 transient failures.
{
"object": "error",
"code": "invalid_request",
"message": "Exactly one of course_id or class_code is required",
"errors": [
{ "param": "request", "message": "Exactly one of course_id or class_code is required" }
]
}Try it
Requests run in your browser to the host you pick. The key stays in this tab unless you opt in to session storage (never sent to UPSked except as the Bearer header on the request you trigger).
URL
/semesters