Reference

Website Integration Guide

Web DeveloperNo CampusOS login required to read this

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.

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.

Nothing changes on the CampusOS side for a new school

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.

No second login screen — the visitor only ever enters their password once, on the website's own page.

POST/auth/login
Request
{
  "user_code": "jdaadm0001",
  "password": "••••••••",
  "school_id": "jnanadegulaacademy"
}
Response (success)
{
  "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.

Why the URL hash, not the query string

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.

Does the account require two-factor authentication?

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.

requires_2fa: false

Tokens are already present — proceed straight to the session-adopt redirect above.

requires_2fa: true

Account already has 2FA enabled. Show a 6-digit code field, then POST /auth/verify-2fa below.

requires_2fa_setup: true

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
Request
{
  "pre_auth_token": "…",   // from the requires_2fa response
  "totp_code": "123456"
}
Response
{
  "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
Request
{ "setup_token": "…" }  // from the requires_2fa_setup response
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:

POST/auth/2fa/confirm-mandatory
Request
{ "setup_token": "…", "totp_code": "123456" }
Response
{
  "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.

This is a standard, repeatable pattern

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
Request
{
  "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"
}
Response
{
  "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
Request
{
  "school_id": "jnanadegulaacademy",
  "reference_no": "INQ/2025-26/0089",
  "parent_phone": "9845012345"
}
Response
{
  "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
}
Do the reference number and phone number both match the same application?

Checked together, server-side, on every lookup — not just a format check on the reference number.

Not found
Wrong reference number, or right reference number with the wrong phone

Both fail identically — a stranger who only knows (or guesses) a reference number learns nothing about whether it's real.

Status returned
Reference number and phone both match one application

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

Deliberately not a CampusOS endpoint

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

  1. Provision the school in CampusOS. Done once, by Stavion's Super Admin console — produces the school's school_id and its first Admin/Principal login.

  2. 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.

  3. Build the login form. Posts to /auth/login with the website's own school_id; on success, redirects to /session-adopt (see Seamless Login).

  4. Build the admission enquiry form. Posts to /admissions/inquiry; shows the returned reference number back to the parent.

  5. Add a status-check form (optional but recommended). Posts to /admissions/track with the reference number and phone the parent submitted with.

  6. Set up the school's own Resend account for general contact. Independent of CampusOS — see General Contact Form.

  7. Add the small "Powered by Stavion" credit to the footer. One muted, low-emphasis line, linking to staviontech.com in 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

All seven are public — no API key, no login, open to any origin
Method & PathPurposeRequires
GET /schools/:school_id/infoSchool name, logo/hero image, and configured class list (and branches, if the school has more than one).Nothing — path only
POST /auth/loginAuthenticate; returns tokens + the user's role, or a 2FA challenge.user_code, password, school_id
POST /auth/verify-2faCompletes login for an account that already has 2FA enabled.pre_auth_token, totp_code
POST /auth/2fa/setup-mandatoryStarts mandatory 2FA setup — returns the QR code/secret/backup codes.setup_token
POST /auth/2fa/confirm-mandatoryConfirms setup with a code and completes login.setup_token, totp_code
POST /admissions/inquirySubmit an admission enquiry into the school's real Admission Pipeline.school_id, child + parent details
POST /admissions/trackCheck 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.

ConstantDev / TestProduction
CAMPUSOS_API_BASE_URLhttps://erpapplication-production.up.railway.app/api/v1That environment's own Railway domain — ask Stavion for the current value.
CAMPUSOS_APP_URLhttps://erp-test.pages.devThe school's own CampusOS subdomain/custom domain, once assigned.
These values aren't published anywhere permanent

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.