@elias4044/ssp-nodeis the official Node.js library for the SchoolSoft+ platform. It wraps SchoolSoft's unofficial internal REST API and gives you a clean, typed interface to authenticate, fetch schedules, assignments, lunch menus, messages, grades, attendance, and more — all without any runtime dependencies beyond Node's built-in modules.
This library targets SchoolSoft's unofficial, reverse-engineered API. Endpoints may change without notice.
Introduction
SchoolSoft+ Node provides two ways to interact with SchoolSoft:
SchoolsoftClient class — A stateful client that holds your session, automatically refreshes tokens, and exposes every endpoint as a method. This is the recommended approach for most use-cases.
Functional API — Every internal function used by the class is also exported individually. Useful for serverless environments, functional codebases, or when you want fine-grained control.
The library uses Node's built-in https module for all HTTP requests — no axios, no node-fetch, no runtime dependencies whatsoever. It is fully typed with TypeScript and ships its own type declarations.
Installation
bash
npm install @elias4044/ssp-node
Requires Node.js 18+ (uses the crypto and https built-ins). Works in CommonJS and ESM projects. TypeScript 5+ is recommended.
Quick start
The fastest path from zero to fetching data is a simple username/password login:
Simple login submits your credentials to SchoolSoft's JSP login form. On success, SchoolSoft returns an HTTP 302 redirect and sets three cookies:JSESSIONID, hash, andusertype. The library captures these automatically.
typescript
const result = await client.login({
username: 'john.doe',
password: 'secret',
usertype: '1', // '1' = student (default), '2' = guardian, '3' = staff
});
console.log(result.jsessionid); // raw JSESSIONID cookie value
console.log(result.hash); // raw hash cookie value
console.log(result.cookieHeader); // 'JSESSIONID=...; hash=...; usertype=1'
login() — options
Name
Type
Required
Description
username
string
Yes
Your SchoolSoft username (typically firstname.lastname).
Simple login sessions are tied to a browser-style JSESSIONID. They may expire after a period of inactivity (typically a few hours). Use verifySession() to check liveness, or prefer the mobile flow for longer-lived sessions.
#Restoring a saved simple-login session
You can persist the cookie values after login and restore them on the next startup without re-authenticating:
typescript
// Save after loginconst { jsessionid, hash, usertype } = await client.login({ ... });
// e.g. store in a file, database, or environment variable// Restore on next startup
client.setSessionCookies(jsessionid, hash, usertype);
// Optionally verify before usingconst alive = await client.verifySession();
if (!alive) await client.login({ ... }); // re-authenticate if expired
Authentication — Mobile login (automated)
The mobile flow mirrors how the official SchoolSoft eApp authenticates — using OAuth2 + PKCE. It gives you a short-lived access token (≈ 15 min) and a long-lived refresh token. You then exchange the access token for session cookies to use the REST API.
typescript
const client = newSchoolsoftClient({ school: 'engelska' });
// Step 1 — obtain access + refresh tokensconst { accessToken, refreshToken, expiresAt } = await client.mobileLogin({
username: 'john.doe',
password: 'secret',
orgid: '18', // school organisation ID, defaults to '18'
});
// Step 2 — (optional) fetch user info to get the userId for a more reliable exchangeconst info = await client.fetchMobileSessionInfo();
// info: { username, firstName, lastName, email, schoolName, userType, userId }// Step 3 — exchange the access token for session cookiesawait client.mobileExchangeSession(info?.userId, {
orgid: '18',
language: 'sw', // 'sw' (Swedish) or 'en'
theme: 'dark',
useros: 'android', // 'android' or 'ios'
});
// Now you can call any data endpointconst schedule = await client.getSchedule();
mobileLogin() — options
Name
Type
Required
Description
username
string
Yes
SchoolSoft username.
password
string
Yes
SchoolSoft password.
orgid
string
No
School organisation ID. Defaults to '18'.
mobileExchangeSession() — options
Name
Type
Required
Description
userId
string | number
No
The user's numeric ID. Pass fetchMobileSessionInfo().userId for a more reliable exchange.
orgid
string
No
School org ID. Defaults to '18'.
language
string
No
'sw' (Swedish, default) or 'en'.
theme
string
No
'dark' (default) or 'light'.
useros
string
No
'android' (default) or 'ios'.
redirectUrl
string
No
Override the post-login redirect URL. Defaults to the subject rooms page.
Authentication — Mobile login (browser / WebView)
For apps that show a login page in a browser or WebView, use the two-step PKCE browser flow. You generate a URL the user opens, then complete the flow after they authenticate and SchoolSoft redirects back to your app.
typescript
import { SchoolsoftClient } from'@elias4044/ssp-node';
// Step 1 — generate the auth URL and PKCE materialconst flow = SchoolsoftClient.startMobileFlow({
school: 'engelska',
redirectUri: 'com.myapp://auth', // your deep-link URI
orgid: '18',
});
console.log(flow.authUrl); // open this in a browser or WebView// flow.verifier — store securely on device// flow.state — store to verify on callback (CSRF protection)// After the user logs in, SchoolSoft redirects to:// com.myapp://auth?code=XXXX&state=YYYY// Verify flow.state === YYYY, then:// Step 2 — complete the flow with the code from the deep-linkconst client = newSchoolsoftClient({ school: 'engelska' });
await client.completeMobileFlow(code, flow.verifier);
// Step 3 — exchange for session cookiesconst info = await client.fetchMobileSessionInfo();
await client.mobileExchangeSession(info?.userId);
const news = await client.getNews();
startMobileFlow() — static method options
Name
Type
Required
Description
school
string
Yes
School slug, e.g. 'engelska'.
redirectUri
string
No
Deep-link redirect URI. Defaults to 'com.schoolsoftplus.app://'.
orgid
string
No
School org ID. Defaults to '18'.
Returns MobileAuthFlowInit with fields:authUrl, verifier,state, school,orgid.
Token management
#Refreshing the access token
Access tokens are short-lived (≈ 15 minutes). Call mobileRefresh() to exchange the stored refresh token for a new access token. This is done automatically by mobileExchangeSession() if the token appears expired, but you can also call it manually.
typescript
// Manual refreshawait client.mobileRefresh();
// Check token state
console.log(client.accessToken); // current access token string or null
console.log(client.refreshToken); // current refresh token or null
console.log(client.isAccessTokenExpired); // boolean
console.log(client.hasMobileToken); // true if an access token is present (any expiry)
mobileExchangeSession() automatically calls mobileRefresh() if isAccessTokenExpiredis true and a refresh token is available. You typically don't need to call it manually.
#Restoring tokens from storage
typescript
// After mobileLogin(), persist these valuesconst toSave = {
accessToken: client.accessToken,
refreshToken: client.refreshToken,
};
// On next startup — restore and proceed without re-logging-inconst client2 = newSchoolsoftClient({ school: 'engelska' });
client2.setAccessToken(toSave.accessToken!, toSave.refreshToken ?? undefined);
// Exchange for session cookies (auto-refreshes if access token expired)await client2.mobileExchangeSession();
Session management
#Verifying a session
verifySession()sends a lightweight request to SchoolSoft's /rest-api/session endpoint to check whether the current session cookies are still valid.
typescript
const alive = await client.verifySession();
// true — session is valid and ready to use// false — session has expired (need to re-authenticate)
#isAuthenticated
The isAuthenticated getter returns true if session cookies are set in memory — it does not verify with SchoolSoft. Use it as a quick guard before making requests, and verifySession() when you need a guaranteed live check.
typescript
if (!client.isAuthenticated) {
await client.login({ username, password });
}
#Logout
logout() clears all in-memory state: session cookies, access token, and refresh token. It does not invalidate anything server-side.
Returns every school registered on SchoolSoft, sorted alphabetically by name. Results are cached in-process for one hour to avoid hammering SchoolSoft on repeated calls. No authentication required.
typescript
const schools = await client.getSchools();
// Returns: School[]// schools[0] might be:// { id: 'engelska', name: 'Engelska Skolan Norr om Ström' }// Find a school slug by nameconst match = schools.find(s => s.name.toLowerCase().includes('carlwahren'));
console.log(match?.id); // 'carlwahren'
Return type: School[]
typescript
interfaceSchool {
name: string; // full display name
id: string; // URL slug used in all API calls
}
getSession()
Returns the full session info for the currently authenticated user from SchoolSoft's /rest-api/session endpoint. Includes user identity, organisation, and user type.
Returns the lesson schedule for the given week. Omitting week defaults to the current ISO week (calculated via the exported isoWeek(date) utility). Lessons are de-duplicated and sorted chronologically.
typescript
// Current weekconst { lessons, week } = await client.getSchedule();
console.log(`Week ${week}: ${lessons.length} lessons`);
// Specific weekconst { lessons: w22 } = await client.getSchedule(22);
// Each lessonfor (const lesson of lessons) {
console.log(lesson.eventId); // unique identifier
console.log(lesson.startDate); // ISO 8601 date-time string
console.log(lesson.endDate); // ISO 8601 date-time string// Additional fields from SchoolSoft (subject name, room, teacher, etc.)// are passed through as-is under the [key: string]: unknown index signature
}
getSchedule() — parameters
Name
Type
Required
Description
week
number
No
ISO week number (1–53). Defaults to current week.
Return type: { lessons: ScheduleLesson[]; week: number }
typescript
interfaceScheduleLesson {
eventId: number | string;
startDate: string; // ISO 8601
endDate: string; // ISO 8601
[key: string]: unknown;
}
The library uses the ISO 8601 week algorithm for the isoWeek() helper, which matches how SchoolSoft numbers weeks (week 1 = first week with a Thursday in the new year). You can import it directly: import { isoWeek } from '@elias4044/ssp-node';
getAssignmentsForWeek(week?, year?)
Returns a flat list of all assignments due in a given week. Both parameters default to the current ISO week and year if omitted. Each object contains the assignment's id and title, plus any additional fields SchoolSoft includes. Pass the ID to getAssignment() to fetch full details.
typescript
// Current week (defaults)const assignments = await client.getAssignmentsForWeek();
// Specific week and yearconst assignments = await client.getAssignmentsForWeek(22, 2025);
// Returns: Assignment[]for (const a of assignments) {
console.log(a.id, a.title);
// Fetch full details for oneconst detail = await client.getAssignment(a.id);
}
getAssignmentsForWeek() — parameters
Name
Type
Required
Description
week
number
No
ISO week number. Defaults to current week.
year
number
No
Full four-digit year. Defaults to current year.
Return type: Assignment[]
getAssignment(id, type?)
Fetches complete details for a single assignment or planning entry. Makes up to five parallel sub-requests to gather all related data (sections, connected plannings, assessment, grading). Returns nullfor sub-resources that don't exist (403/404 are handled gracefully).
typescript
// Default: fetch an assignmentconst detail = await client.getAssignment(12345);
// Fetch a planning entry insteadconst planning = await client.getAssignment(67890, 'planning');
// Full shape
console.log(detail.assignment); // view object from SchoolSoft
console.log(detail.sections); // sections array or null
console.log(detail.connectedPlannings); // linked planning entries or null
console.log(detail.assessment?.review); // teacher review text
console.log(detail.assessment?.teacherComment); // teacher comment
console.log(detail.assessment?.studentComment); // student comment
console.log(detail.assessment?.assessedCriteriaTabs); // graded criteria
console.log(detail.assessment?.partialMoments); // partial assessment moments
console.log(detail.grading); // grading object or null
Returns non-lesson calendar events for the given week — such as school holidays, custom reminders, and other entries that appear on the SchoolSoft calendar but are not regular lessons. Defaults to the current ISO week.
typescript
// Current weekconst events = await client.getCalendarEvents();
// Specific weekconst events = await client.getCalendarEvents(22);
// Returns: CalendarEvent[]for (const ev of events) {
console.log(ev.title); // event title
console.log(ev.startDate); // ISO 8601 string or undefined
console.log(ev.endDate); // ISO 8601 string or undefined
console.log(ev.allDay); // boolean — true for all-day entries
console.log(ev.type); // event type string from SchoolSoft
}
Returns detailed information about a single subject room, including its examination overview, submission status, and all linked assignments.
typescript
const detail = await client.getSubject('123');
console.log(detail.subject); // subject metadata
console.log(detail.overview.examinations); // upcoming examinations
console.log(detail.overview.submissions); // pending submissions
console.log(detail.assignments); // all assignments for the subject
getSubject() — parameters
Name
Type
Required
Description
id
string | number
Yes
Subject room ID (from getSubjects()).
Return type: SubjectDetail
getNews()
Scrapes news items from the student startpage HTML. Returns an array of news items with an ID, title, and a short preview text.
typescript
const news = await client.getNews();
// Returns: NewsItem[]for (const item of news) {
console.log(item.id); // news item ID (string) or null if not found
console.log(item.title); // news headline
console.log(item.preview); // short preview text or null
}
Return type: NewsItem[]
getNewsDetail(id)
Fetches the full content of a single news article by its ID. Returns null if the article is not found.
typescript
const article = await client.getNewsDetail('42');
if (article) {
console.log(article.id); // '42'
console.log(article.title); // article headline
console.log(article.body); // raw HTML body of the article
console.log(article.date); // date string or null
console.log(article.attachments); // [{ name, url }, ...]
}
getNews(), getNewsDetail(), getStartpage(), and getClassStudents()scrape HTML pages rather than calling JSON endpoints. They depend on SchoolSoft's page structure and may break if SchoolSoft changes their HTML layout.
getStartpage()
Returns upcoming homework and recent test results from the student startpage by scraping the HTML. Useful for a quick daily overview.
typescript
const { homework, tests } = await client.getStartpage();
// Upcoming homeworkfor (const hw of homework) {
console.log(hw.dateAndSubject); // e.g. 'Monday — Mathematics'
console.log(hw.title); // assignment/homework title
console.log(hw.content); // string[] — lines of the description
}
// Recent test resultsfor (const test of tests) {
console.log(test.title); // test name
console.log(test.description); // grade / result text
console.log(test.link); // URL to the test detail page, if available
}
Returns the list of students in the authenticated user's class. Scrapes the student directory HTML page.
typescript
const students = await client.getClassStudents();
// Returns: ClassStudent[]for (const s of students) {
console.log(s.name); // full display name
console.log(s.email); // email address, or null
console.log(s.address); // home address, or null
}
Returns the list of messages in the authenticated user's inbox.
typescript
const messages = await client.getInbox();
// Returns: Message[]for (const msg of messages) {
console.log(msg.id); // message ID
console.log(msg.subject); // message subject line
console.log(msg.senderName); // display name of the sender
console.log(msg.date); // date string from SchoolSoft
console.log(msg.read); // boolean — whether the message has been read
}
Return type: Message[]
getOutbox()
Returns the list of messages the authenticated user has sent.
typescript
const sent = await client.getOutbox();
// Returns: Message[]
Fetches the full content of a single message, including its body, recipients, and any attachments. Returns null if not found.
typescript
const detail = await client.getMessage(42);
if (detail) {
console.log(detail.subject); // subject line
console.log(detail.body); // full message body text
console.log(detail.recipients); // MessageRecipient[]
console.log(detail.attachments); // MessageAttachment[]
}
Returns an aggregated attendance summary, grouped by subject. Includes total lessons, total absences, and an overall attendance rate percentage.
typescript
const summary = await client.getAbsenceSummary();
console.log(summary.totalLessons); // e.g. 200
console.log(summary.totalAbsences); // e.g. 12
console.log(summary.attendanceRate); // e.g. 94 (percentage 0–100)for (const sub of summary.subjects) {
console.log(sub.subject); // subject name
console.log(sub.totalLessons); // total lessons for this subject
console.log(sub.absences); // absences for this subject
}
Returns all individual grade entries for the authenticated student.
typescript
const grades = await client.getGrades();
// Returns: GradeEntry[]for (const g of grades) {
console.log(g.subjectName); // e.g. 'Mathematics'
console.log(g.grade); // e.g. 'A', 'B', 'E'
console.log(g.date); // date the grade was set
console.log(g.teacherName); // grading teacher
console.log(g.comment); // optional comment
}
Every function used internally by SchoolsoftClient is also exported at the top level. This is useful for serverless functions, functional-style code, or when you want to manage session state yourself.
All data functions share the same signature pattern:
typescript
getXxx(
school: string, // school slug, e.g. 'engelska'
cookieHeader: string, // 'JSESSIONID=...; hash=...; usertype=1'
...args, // endpoint-specific arguments
userAgent: string, // User-Agent header value
): Promise<ReturnType>
Advanced — client.raw()
Makes a raw authenticated request to any SchoolSoft endpoint using the client's stored session cookies. Useful for endpoints not yet covered by the library. The path can be an absolute URL or a root-relative path (starting with /).
typescript
// Root-relative path — the school slug is prepended automaticallyconst result = await client.raw('/rest-api/student/some/endpoint', {
method: 'GET',
responseType: 'json',
});
console.log(result.status, result.data);
// Absolute URL — used as-isconst result2 = await client.raw<MyType>(
'https://sms.schoolsoft.se/engelska/rest-api/student/endpoint',
{ responseType: 'json' }
);
Advanced — Raw requests (schoolsoftFetch)
schoolsoftFetch is the underlying HTTP client used by every function in the library. It automatically sets the correct Origin, Referer, and User-Agent headers required by SchoolSoft, and parses the response according to the requested responseType.
typescript
import { schoolsoftFetch, ssUrl } from'@elias4044/ssp-node';
// ssUrl() builds a correctly structured SchoolSoft URLconst url = ssUrl('engelska', '/rest-api/student/calendar/lessons/week/22');
// → 'https://sms.schoolsoft.se/engelska/rest-api/student/calendar/lessons/week/22'const result = await schoolsoftFetch<MyResponseType>(url, 'engelska', {
method: 'GET',
headers: { Cookie: cookieHeader, Accept: 'application/json' },
responseType: 'json', // 'json' | 'text' | 'buffer'
followRedirects: true, // default true — set false to capture 302s
}, 'my-app/1.0'); // optional User-Agent
console.log(result.status); // HTTP status code
console.log(result.data); // parsed body (type T)
console.log(result.headers); // lowercase key map of all response headers
console.log(result.setCookies); // string[] — raw Set-Cookie values
For even lower-level access, rawRequest() is the underlying Node https wrapper used by schoolsoftFetch. It follows redirects (up to 5 hops), preserves all Set-Cookie headers, and returns the raw Buffer body.
The PKCE helpers used internally by the mobile auth flow are exported for use in your own OAuth2 implementations. All crypto uses Node's built-in crypto module — no external dependencies.
typescript
import { makePkcePair, makeState, base64url } from'@elias4044/ssp-node';
// Generate a PKCE verifier + SHA-256 challenge pairconst { verifier, challenge } = makePkcePair();
// verifier — random 32-byte base64url string, kept secret on device// challenge — SHA-256(verifier) encoded as base64url, sent to auth server// Generate a random CSRF state tokenconst state = makeState();
// → 24-char hex string, e.g. 'a3f91c7e2b84d06f1234abcd'// base64url encode arbitrary dataconst encoded = base64url(Buffer.from('hello world'));
Advanced — Token storage pattern
For apps that need to survive restarts without the user logging in again — mobile apps, daemons, bots — persist the refresh token and restore it on startup.
typescript
import { SchoolsoftClient } from'@elias4044/ssp-node';
import fs from'fs';
constTOKEN_FILE = '.ssp-tokens.json';
const school = 'engelska';
asyncfunction getAuthenticatedClient(): Promise<SchoolsoftClient> {
const client = newSchoolsoftClient({ school });
// Try to restore saved tokensif (fs.existsSync(TOKEN_FILE)) {
const saved = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8'));
client.setAccessToken(saved.accessToken, saved.refreshToken, saved.expiresAt);
try {
// This auto-refreshes if access token is expiredawait client.mobileExchangeSession();
if (await client.verifySession()) {
console.log('Session restored from file.');
return client;
}
} catch {
console.log('Saved session invalid — re-logging in.');
}
}
// Fall back to full loginconst result = await client.mobileLogin({
username: process.env.SS_USER!,
password: process.env.SS_PASS!,
});
// Persist tokens for next run
fs.writeFileSync(TOKEN_FILE, JSON.stringify({
accessToken: result.accessToken,
refreshToken: result.refreshToken,
expiresAt: result.expiresAt,
}));
await client.mobileExchangeSession();
return client;
}
const client = await getAuthenticatedClient();
const lunch = await client.getLunch(22);
Never commit token files to source control. Add .ssp-tokens.json to your .gitignore. In production, use a secrets manager or environment variables with encrypted storage.
Advanced — Error handling
All errors thrown by the library extend the base SchoolsoftError class, which carries the HTTP status code and endpoint URL. This lets you catch and inspect errors programmatically without relying on message strings.
School slug from the URL (between sms.schoolsoft.se/ and the next /). Defaults to 'engelska'. Use getSchools() to find yours.
userAgent
string
No
Custom User-Agent header sent with all requests.
debug
boolean
No
Emit debug messages to stderr. Defaults to false.
retry
boolean | RetryOptions
No
Automatic retry on transient failures. Pass true for defaults, false to disable, or a RetryOptions object. Enabled by default with 3 attempts.
cache
false | { schools?: number }
No
Cache config. Set false to disable, or pass an object with a schools TTL in milliseconds (default 3 600 000 = 1 hour).
onSessionExpired
() => void | Promise<void>
No
Callback invoked when a SessionExpiredError is detected. Use it to trigger re-authentication in long-running processes.
typescript
// RetryOptions — all fields optionalinterfaceRetryOptions {
maxAttempts?: number; // total attempts including the first. Default: 3
initialDelay?: number; // ms before the first retry. Default: 500
backoffFactor?: number; // multiplier after each attempt. Default: 2
maxDelay?: number; // upper bound on delay in ms. Default: 10 000
retryOn?: number[]; // HTTP status codes to retry. Default: [429, 502, 503, 504]
}
The school slug is the only meaningful piece of configuration for most use cases. You can find your school's slug by visiting https://sms.schoolsoft.se and looking at the URL after you select your school, or by calling getSchools():