StarSchedule's Open API lets scripts, plugins, or automation tools read your own timetables, memos, and class notes over HTTP, and compare free time with a study buddy's pinned timetable. Authentication uses an App password, created under Me → Open API. Delete or rotate it independently without affecting your sign-in password.
Username = your Star ID, password = an App password generated on the web or in the app.
Up to 5 per account. Rotate or delete at any time; the old password stops working immediately.
Reads only accepted partnerships; a partner's timetable is limited to their current pinned timetable.
60 requests per minute per password; over that returns 429.
All /v1/open/* endpoints require HTTP Basic Auth:
Authorization: Basic base64("Star ID:App password")Passing `-u` to `curl` handles the base64 encoding for you:
curl -u "your-star-id:your-app-password" \ https://starschedule.xingqingchuang.com/api/v1/open/schedules
Authentication failures always return 401 + { code: 401, message, data: null }, with the response header WWW-Authenticate: Basic. Memos, class notes, partner timetables, and free time all use the same App password; access is limited to the account's own data, with partner data only within accepted partnerships.
An app password starts with the read scope, covering every read endpoint. Tick the write option to also change your memos, todos and class notes; a read-only password gets 403 on a write.
read — Read your own timetables, memos, notes, grades and campus data.write:daily — Also create, update and delete your memos, todos and class notes.Every endpoint below is described in an OpenAPI 3.1 document, generated from the server function registry, so it never drifts from the implementation.
openapi.json/v1/open/schedules{
"code": 0,
"message": "ok",
"data": {
"schedules": [
{ "uid": "abcd1234...", "name": "2025 春季" },
{ "uid": "efgh5678...", "name": "实习课表" }
]
}
}/v1/open/schedules/{uid}include(query)— Comma-separated sub-sections. Options: courses / adjustments / exams / clubs. Omit to get all.uid(path)— The schedule uid returned by the endpoint above.{
"code": 0,
"message": "ok",
"data": {
"schedule": {
"uid": "abcd1234...",
"name": "2025 春季",
"term": {
"uid": "term-...",
"name": "2024-2025 第二学期",
"startDate": "2025-02-24",
"totalWeeks": 18
},
"courses": [ /* course_uid, name, color, credit, note ... */ ],
"courseSessions": [ /* weekday, start/end section, start/end week, location, teacher ... */ ],
"temporaryAdjustments": [ /* 停课 / 换时间 / 换地点 */ ],
"exams": [ /* 考试日程 */ ],
"clubActivities": [ /* 社团活动 */ ]
},
"meta": { "include": ["adjustments", "clubs", "courses", "exams"] }
}
}/v1/open/memosfrom / to(query)— Filter by date, formatted as YYYY-MM-DD.updatedSince(query)— Return only records updated after this time, for incremental sync.todo(query)— Optional true / false; filter by to-do status.{
"code": 0,
"message": "ok",
"data": {
"items": [
{
"uid": "memo-uid",
"date": "2025-03-12",
"content": "提交实验报告",
"color": "#4F46E5",
"isTodo": true,
"completedAt": null,
"deadlineAt": "2025-03-15T23:59:00+08:00",
"reminderAt": null,
"createdAt": "2025-03-01T08:00:00Z",
"updatedAt": "2025-03-12T10:00:00Z"
}
],
"nextCursor": null
}
}/v1/open/class-notesscheduleUid / courseUid / sessionUid(query)— Optional timetable, course, and session UIDs. Only the current account's own data can be read.occurrenceDate(query)— Filter by the date the class took place, formatted as YYYY-MM-DD.include(query)— Optional assets,topic. assets returns image asset metadata (uid / mimeType / sort); image file downloads aren't offered through the Open API yet.{
"code": 0,
"message": "ok",
"data": {
"items": [
{
"uid": "note-uid",
"scheduleUid": "schedule-uid",
"courseUid": "course-uid",
"sessionUid": "session-uid",
"occurrenceDate": "2025-03-12",
"content": "老师强调了递归边界条件。",
"topic": "算法基础",
"createdAt": "2025-03-12T15:30:00Z",
"updatedAt": "2025-03-12T15:40:00Z",
"assets": []
}
],
"nextCursor": null
}
}/v1/open/buddies{
"code": 0,
"message": "ok",
"data": {
"items": [
{
"linkId": "buddy-link-uid",
"peer": {
"sid": "ABC12345",
"nickname": "小明",
"school": "某某大学",
"avatarUrl": null
},
"state": "accepted",
"hasPinnedSchedule": true,
"updatedAt": "2025-03-09T17:30:00Z"
}
],
"nextCursor": null
}
}/v1/open/buddies/{linkId}/schedulelinkId(path)— The relationship ID returned by the buddy list; the relationship must belong to the current account and be accepted.{
"code": 0,
"message": "ok",
"data": {
"buddy": {
"linkId": "buddy-link-uid",
"sid": "ABC12345",
"nickname": "小明"
},
"pinnedSchedule": {
"uid": "schedule-uid",
"name": "2025 春季",
"term": {
"name": "2024-2025 第二学期",
"startDate": "2025-02-24",
"totalWeeks": 18
},
"courses": [ /* 课程和上课节次 */ ],
"temporaryAdjustments": [ /* 停课 / 换时间 / 换地点 */ ],
"timeScheme": { "sections": [ /* 节次时间 */ ] }
}
}
}/v1/open/free-time/compareselfScheduleUid(body)— The current account's own timetable UID.linkIds(body)— List of partnership IDs. The server reads each partner's pinned timetable automatically; you can't pass a partner scheduleUid.dateFrom / dateTo(body)— Date range, up to 31 days.{
"code": 0,
"message": "ok",
"data": {
"basis": "section",
"timezone": "Asia/Shanghai",
"participantCount": 2,
"slots": [
{
"date": "2025-03-10",
"weekday": 1,
"week": 3,
"startSection": 3,
"endSection": 4,
"startTime": "10:10",
"endTime": "11:50",
"durationMinutes": 100
}
],
"warnings": []
}
}The partner endpoints only allow access to accepted relationships. A partner's timetable is always resolved server-side from their current pinned timetable; no other timetables of theirs can be queried. The free-time endpoint returns only the shared free slots of all participants — never who is busy when, course names, teachers, or locations. If a partner hasn't set a pinned timetable, no other timetable is substituted: the endpoint skips them and says so in warnings.
GET /v1/open/meGET /v1/open/schedulesGET /v1/open/schedules/{uid}GET /v1/open/schedules/{uid}/coursesGET /v1/open/schedules/{uid}/time-schemeGET /v1/open/schedules/{uid}/adjustmentsGET /v1/open/schedules/{uid}/examsGET /v1/open/schedules/{uid}/clubsGET /v1/open/termsGET /v1/open/timetable/currentGET /v1/open/timetable/occurrencesGET /v1/open/examsGET /v1/open/club-activitiesGET /v1/open/memosGET /v1/open/todosGET /v1/open/class-notesGET /v1/open/campusGET /v1/open/campus/gradesGET /v1/open/campus/attendanceGET /v1/open/campus/seatsGET /v1/open/snapshot/summaryGET /v1/open/snapshotGET /v1/open/buddiesGET /v1/open/buddies/{linkId}/schedulePOST /v1/open/free-time/comparePOST /v1/open/memosPATCH /v1/open/memos/{uid}DELETE /v1/open/memos/{uid}POST /v1/open/todosPATCH /v1/open/todos/{uid}POST /v1/open/todos/{uid}/completeDELETE /v1/open/todos/{uid}POST /v1/open/class-notesPATCH /v1/open/class-notes/{uid}DELETE /v1/open/class-notes/{uid}Each App password allows 60 requests per minute in a rolling window. Going over returns 429 + { code: 429, message, data: null }, with the response header RateLimit-* fields (draft-7) describing your remaining quota.
If you need a higher rate (for dashboard polling, say), cache responses locally and use Last-Modified semantics to slow yourself down, rather than hitting the API repeatedly.
| HTTP | code | Meaning |
|---|---|---|
| 401 | 401 | No credentials / wrong Star ID or password / password revoked |
| 404 | 404 | The timetable uid doesn't exist or belongs to another account |
| 403 | 403 | No permission for that data, or no access to that partnership |
| 409 | 409 | The partnership hasn't been accepted (not connected). If a partner has no pinned timetable it doesn't error: it returns pinnedSchedule: null or explains it in warnings. |
| 429 | 429 | A single password exceeded 60 requests per minute |
| 500 | 500 | The server couldn't read the timetable. Please try again later. |