Website Integration Guide
Overview
Every school on CampusOS is free to run its own public marketing website, on its own domain, built however it likes. This guide is what to hand that website's developer — whether that's you, an agency, or the school's existing vendor — so their site can talk to CampusOS directly: a real login that lands a parent or staff member already signed in, a real admission enquiry that lands in the school's actual Admission Pipeline, and a way for a parent to check that application's status later. One school, Jnana Degula Academy, already runs on exactly this pattern — the examples below are the real endpoints and payloads its website uses.
Key concepts
- 1. school_idThe one thing that ties a website to a specific school's data. It's a slug (e.g.
jnanadegulaacademy), assigned once when the school is provisioned in CampusOS, and never entered by a visitor — the website's own code hardcodes it, so every request it sends already knows which school it belongs to. - 2. Public endpointsThe four endpoints in this guide need no API key and no login — they're designed to be called directly from a browser, from any origin. There is nothing to request access to and nothing to keep secret to use them.
- 3. Session hand-offCampusOS's own dashboard only exists at the CampusOS app's own address. A website's login form authenticates the visitor, then redirects the browser there already signed in — see Seamless Login for exactly how.
Architecture
A school's website and CampusOS are two separate deployments that never share a codebase or a server — the website calls CampusOS the same way any browser would, over its public API.
School's own website
Any stack, any host. Holds one constant: its own school_id.
CampusOS public API
Login, admission enquiry, status check, school info — no auth, open to any origin.
That school's own data
The school_id in every request resolves to exactly one school's own records — never another school's.
Every request the website sends carries its own school_id — CampusOS never needs to be told which school a new website belongs to beyond that.
Provisioning a new school's tenant (Super Admin's job) is the only setup step. The endpoints in this guide don't know or care how many schools' websites call them — connecting a second, third, or fiftieth school's website needs zero changes here, only that school's own school_id.
Seamless Login
The website's own login form collects a user ID and password and authenticates them for real — there is no separate "preview" login and no second sign-in step on CampusOS's side. On success, the browser is handed off to the CampusOS app already signed in.
Visitor signs in
User ID/email + password, submitted on the school's own website.
Website calls the API
POST /auth/login with the credentials plus the website's own school_id.
Browser redirected
To /session-adopt on the CampusOS app, tokens carried in the URL's hash fragment.
Already signed in
CampusOS reads the hash once, scrubs it from the address bar, opens the right dashboard.
No second login screen — the visitor only ever enters their password once, on the website's own page.
POST/auth/login{
"user_code": "jdaadm0001",
"password": "••••••••",
"school_id": "jnanadegulaacademy"
}
{
"requires_2fa": false,
"access_token": "…",
"refresh_token": "…",
"expires_in": 900,
"user": {
"id": "…",
"role": "school_admin",
"school_id": "jnanadegulaacademy",
"first_name": "…",
"…": "…"
}
}
Redirect the browser to {CAMPUSOS_APP_URL}/session-adopt#at={access_token}&rt={refresh_token}&u={encodeURIComponent(JSON.stringify(user))}
— a full navigation (window.location.href), not a client-side route change, since it's crossing
to CampusOS's own origin.
Everything after # in a URL is never sent to any server and never appears in a CDN or proxy's access log — only client-side JavaScript can read it. CampusOS's session-adopt page reads it once, hydrates the session, and immediately scrubs it from the address bar. Tokens are short-lived regardless, but the hash keeps them off the wire entirely, which the query string cannot.
The enterprise-standard approach: handle both cases inline, on the website's own page — a visitor should never see CampusOS's own login screen, 2FA or not.
Tokens are already present — proceed straight to the session-adopt redirect above.
Account already has 2FA enabled. Show a 6-digit code field, then POST /auth/verify-2fa below.
Account's role mandates 2FA but it isn't set up yet. Show the QR/backup-codes screen, then POST /auth/2fa/setup-mandatory + /confirm-mandatory below.
Already has 2FA enabled — verify a code
POST/auth/verify-2fa{
"pre_auth_token": "…", // from the requires_2fa response
"totp_code": "123456"
}
{
"access_token": "…",
"refresh_token": "…",
"expires_in": 900,
"user": { "…": "…" }
}
Same shape as a successful /auth/login — feed it straight into the same session-adopt redirect.
2FA mandated but not set up yet
POST/auth/2fa/setup-mandatory{ "setup_token": "…" } // from the requires_2fa_setup response
{
"secret": "JBSWY3DPEHPK3PXP",
"qr_code_url": "data:image/png;base64,…",
"backup_codes": ["…", "…", "…"]
}
Render the QR code (an <img> pointed straight at qr_code_url — it's already a data URI, no extra fetch needed), show the backup codes once, then collect a 6-digit code to confirm:
{ "setup_token": "…", "totp_code": "123456" }
{
"access_token": "…",
"refresh_token": "…",
"expires_in": 900,
"user": { "…": "…" }
}
Same response shape again — same session-adopt redirect. Every path through login (plain, verify, or setup) ends at exactly one place.
Nothing above branches on which school is calling it — every field is generic, parameterized only by school_id in the request body. A second school's website reuses this exact login implementation unchanged; only its own school_id constant differs (see "Setup Checklist" below).
Admission Enquiry
A school's own admission/"Apply Now" form can post directly to CampusOS — every submission becomes a real entry in that school's Admission Pipeline, not just an email.
POST/admissions/inquiry{
"school_id": "jnanadegulaacademy",
"child_name": "Aditi Rao",
"class_applying": "Class 3",
"parent_name": "Suresh Rao",
"parent_phone": "9845012345",
"parent_email": "suresh.rao@example.com",
"source": "website"
}
{
"id": "…",
"status": "new",
"reference_no": "INQ/2025-26/0089"
}
Show the returned reference_no back to the parent — it's what they'll use later to check
status (see next section). class_applying can be a plain text field, or — for a nicer form —
read from GET /schools/:school_id/info's classes
list, so the dropdown always matches whatever the school has actually configured, with no separate list to
keep in sync by hand.
Application Status Check
Lets a parent check their own application's progress later, without a CampusOS login — reference number alone isn't accepted as proof of identity, since a parent shares theirs with no one else's information exposed by it.
POST/admissions/track{
"school_id": "jnanadegulaacademy",
"reference_no": "INQ/2025-26/0089",
"parent_phone": "9845012345"
}
{
"reference_no": "INQ/2025-26/0089",
"child_name": "Aditi Rao",
"class_applying": "Class 3",
"status": "under_review",
"status_label": "Under Review",
"submitted_at": "2026-08-01T09:12:00Z",
"stages": [ { "status": "new", "label": "Received" }, "…" ],
"current_stage_index": 1
}
Checked together, server-side, on every lookup — not just a format check on the reference number.
Both fail identically — a stranger who only knows (or guesses) a reference number learns nothing about whether it's real.
Returns the full status payload shown above, including where it sits in the pipeline.
stages is the ordered, non-rejected path (new → under_review → interview_scheduled →
selected → enrolled); current_stage_index is -1 when status is
rejected, since that can happen from any stage — render it as its own outcome rather than a
point on the progress bar.
General Contact Form
A general "Contact Us" message (a question about admissions hours, a lost-and-found query, anything not tied to one specific application) has no natural home in CampusOS — it isn't a lead in the Admission Pipeline, and it isn't tied to any student record. The website should email the school directly for this one, using its own emailing provider (e.g. Resend) called from a small function/endpoint the website hosts itself. Keeping it separate means a school's front-desk inbox doesn't depend on CampusOS being reachable, and CampusOS doesn't need a generic "message" concept it has no use for anywhere else.
Setup Checklist for a New School's Website
Provision the school in CampusOS. Done once, by Stavion's Super Admin console — produces the school's
school_idand its first Admin/Principal login.Add the school's own constants to the website's codebase. Three values: the
school_id, the CampusOS API base URL, and the CampusOS app URL (see Environments) — the only place this school's identity lives.Build the login form. Posts to
/auth/loginwith the website's ownschool_id; on success, redirects to/session-adopt(see Seamless Login).Build the admission enquiry form. Posts to
/admissions/inquiry; shows the returned reference number back to the parent.Add a status-check form (optional but recommended). Posts to
/admissions/trackwith the reference number and phone the parent submitted with.Set up the school's own Resend account for general contact. Independent of CampusOS — see General Contact Form.
Add the small "Powered by Stavion" credit to the footer. One muted, low-emphasis line, linking to
staviontech.comin a new tab — small enough to never compete with the school's own branding, present on every CampusOS-connected site so a visitor can always find who built it. The school's own name and logo stay the prominent identity everywhere else; this is the only Stavion mark on the public site.<a href="https://staviontech.com" target="_blank" rel="noopener noreferrer"> Powered by Stavion </a>
Endpoint Reference
| Method & Path | Purpose | Requires |
|---|---|---|
GET /schools/:school_id/info | School name, logo/hero image, and configured class list (and branches, if the school has more than one). | Nothing — path only |
POST /auth/login | Authenticate; returns tokens + the user's role, or a 2FA challenge. | user_code, password, school_id |
POST /auth/verify-2fa | Completes login for an account that already has 2FA enabled. | pre_auth_token, totp_code |
POST /auth/2fa/setup-mandatory | Starts mandatory 2FA setup — returns the QR code/secret/backup codes. | setup_token |
POST /auth/2fa/confirm-mandatory | Confirms setup with a code and completes login. | setup_token, totp_code |
POST /admissions/inquiry | Submit an admission enquiry into the school's real Admission Pipeline. | school_id, child + parent details |
POST /admissions/track | Check an enquiry's current pipeline status. | school_id, reference_no, parent_phone |
Environments
The same website codebase moves between environments by changing exactly these two values — nothing else about the integration changes.
| Constant | Dev / Test | Production |
|---|---|---|
CAMPUSOS_API_BASE_URL | https://erpapplication-production.up.railway.app/api/v1 | That environment's own Railway domain — ask Stavion for the current value. |
CAMPUSOS_APP_URL | https://erp-test.pages.dev | The school's own CampusOS subdomain/custom domain, once assigned. |
They live in each environment's own hosting configuration (Railway, Cloudflare Pages), not in any codebase — ask Stavion Technologies for the current value for the environment you're integrating against.