Open API

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.

HTTP Basic Auth

Username = your Star ID, password = an App password generated on the web or in the app.

Independently revocable

Up to 5 per account. Rotate or delete at any time; the old password stops working immediately.

Partner authorization

Reads only accepted partnerships; a partner's timetable is limited to their current pinned timetable.

Rate limit 60/min

60 requests per minute per password; over that returns 429.

Authentication

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.

Permissions

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.
OpenAPI description

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

Endpoints

GET/v1/open/schedules
List every timetable in the current account that hasn't been deleted.
Response example
{
  "code": 0,
  "message": "ok",
  "data": {
    "schedules": [
      { "uid": "abcd1234...", "name": "2025 春季" },
      { "uid": "efgh5678...", "name": "实习课表" }
    ]
  }
}
GET/v1/open/schedules/{uid}
Read the detailed data for a single timetable.
Parameters
  • 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.
Response example
{
  "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"] }
  }
}

Extended endpoints

GET/v1/open/memos
Read the current account's memos (daily records).
Parameters
  • from / 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.
Response example
{
  "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
  }
}
GET/v1/open/class-notes
Read the current account's class notes; filter by timetable, course, session, or class date.
Parameters
  • scheduleUid / 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.
Response example
{
  "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
  }
}
GET/v1/open/buddies
List the partnerships the current account has established. Only accepted partnerships are returned.
Response example
{
  "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
  }
}
GET/v1/open/buddies/{linkId}/schedule
Return a partner's current pinned timetable. Their other timetables can't be queried.
Parameters
  • linkId(path)— The relationship ID returned by the buddy list; the relationship must belong to the current account and be accepted.
Response example
{
  "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": [ /* 节次时间 */ ] }
    }
  }
}
POST/v1/open/free-time/compare
Compare free time shared by the current account and one or more partners' pinned timetables.
Parameters
  • selfScheduleUid(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.
Response example
{
  "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": []
  }
}
Partner data safety boundaries

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.

All endpoints

Identity: confirm the password and see its scopes.
  • GET /v1/open/me
Timetables: the current view, a date range of classes, and each timetable's courses, terms, time scheme and exams.
  • GET /v1/open/schedules
  • GET /v1/open/schedules/{uid}
  • GET /v1/open/schedules/{uid}/courses
  • GET /v1/open/schedules/{uid}/time-scheme
  • GET /v1/open/schedules/{uid}/adjustments
  • GET /v1/open/schedules/{uid}/exams
  • GET /v1/open/schedules/{uid}/clubs
  • GET /v1/open/terms
  • GET /v1/open/timetable/current
  • GET /v1/open/timetable/occurrences
  • GET /v1/open/exams
  • GET /v1/open/club-activities
Daily: memos, todos and class notes, each with filters and incremental reads.
  • GET /v1/open/memos
  • GET /v1/open/todos
  • GET /v1/open/class-notes
Campus: grades, attendance and course seats from your campus records.
  • GET /v1/open/campus
  • GET /v1/open/campus/grades
  • GET /v1/open/campus/attendance
  • GET /v1/open/campus/seats
Everything: a per-section change summary, and the whole sync snapshot in one call.
  • GET /v1/open/snapshot/summary
  • GET /v1/open/snapshot
Study buddies: the accepted links, a buddy's pinned timetable and the free-time comparison.
  • GET /v1/open/buddies
  • GET /v1/open/buddies/{linkId}/schedule
  • POST /v1/open/free-time/compare
Writes (needs write:daily): create, update and delete your memos, todos and class notes.
  • POST /v1/open/memos
  • PATCH /v1/open/memos/{uid}
  • DELETE /v1/open/memos/{uid}
  • POST /v1/open/todos
  • PATCH /v1/open/todos/{uid}
  • POST /v1/open/todos/{uid}/complete
  • DELETE /v1/open/todos/{uid}
  • POST /v1/open/class-notes
  • PATCH /v1/open/class-notes/{uid}
  • DELETE /v1/open/class-notes/{uid}
Rate limits

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.

Error codes
HTTPcodeMeaning
401401No credentials / wrong Star ID or password / password revoked
404404The timetable uid doesn't exist or belongs to another account
403403No permission for that data, or no access to that partnership
409409The 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.
429429A single password exceeded 60 requests per minute
500500The server couldn't read the timetable. Please try again later.