Demonstration environment. Fictional records. Not connected to a live school system.

Champ Integration

Admissions API contract 2026-09-13.v1

This project is the receiver (Project B). Every request carries X-Champ-Contract-Version: 2026-09-13.v1; any other value returns 409 unsupported_contract_version.

Endpoints

Layer 1 — connector key

Generated by an owner in Staff → Settings and shown once. Format cak_<uuid>.<secret>. Only SHA-256(secret) is stored. On each request the row is found by the uuid and the hash is compared timing-safely; the key must be active and the organisation_ref must be on its allow-list, otherwise 403 forbidden_organisation. A connector key never unlocks parent records.

Project A configuration (names only): ADMISSIONS_API_BASE_URL, ADMISSIONS_CONNECTOR_KEY.

Layer 2 — parent session hand-off

  1. Project A opens a popup to {ADMISSIONS_API_BASE_URL}/parent/sign-in?origin=<Project A origin>&state=<one-time value>. The origin is not a secret; it only says where the result may be delivered. state is optional, opaque and generated by Project A per attempt (1–256 chars, A-Z a-z 0-9 . _ ~ -); it is never interpreted here.
  2. The parent signs in here with their own email and password. Project A never sees the password.
  3. Project B checks the requested origin against the organisation's approved hand-off list (owner-managed). If it is not approved, no session is issued to that page.
  4. Project B mints cps_<uuid>.<secret>, stores SHA-256(secret) with the parent id, organisation id and an expiry 15 minutes ahead, and delivers it with window.opener.postMessage(message, approvedOrigin) — the exact approved origin, never *, never in a URL, query string or chat message. The state value is echoed back unchanged. The popup then closes.
// Message received by Project A (browser)
{
  "type": "champ.parent_session",
  "contract_version": "2026-09-13.v1",
  "token": "cps_<uuid>.<secret>",
  "expires_at": "2026-09-13T08:35:00.000Z",
  "parent_display_name": "Naledi Moeng",
  "state": "<the same value Project A sent>"   // present only when ?state= was supplied
}

What Project A must do with it

window.addEventListener("message", (e) => {
  if (e.origin !== ADMISSIONS_ORIGIN) return;               // only accept from Project B
  if (e.data?.type !== "champ.parent_session") return;
  if (e.data.contract_version !== "2026-09-13.v1") return;
  if (e.data.state !== expectedState) return;               // must match the one-time value you generated
  // Consume expectedState so it cannot be replayed.
  // Send the token to Project A's own server over HTTPS and keep it there,
  // bound to the visitor's chat session (e.g. server-side store or HttpOnly cookie).
  // Never print it in the chat, never put it in a URL, never store it in the transcript.
});

// Server-to-server call from Project A's backend:
fetch(ADMISSIONS_API_BASE_URL + "/api/public/admissions/v1/parent/enquiries", {
  headers: {
    "X-Champ-Connector-Key": ADMISSIONS_CONNECTOR_KEY,
    "X-Champ-Contract-Version": "2026-09-13.v1",
    "X-Champ-Parent-Authorization": token,
  },
});

On 401 parent_session_invalid (expired, tampered, or revoked) Project A should prompt the parent to sign in again. Never ask a parent to type or paste a token into the chat.

How Project B verifies a parent session

Submitting while signed in

Send X-Champ-Parent-Authorization together with the connector key and set parent_session_present: true. The flag is only a declaration:

Idempotency and conflicts

Contract additions in this implementation

Account linking for guest enquiries

A guest enquiry is never linked because an email or phone matches. Staff verify the parent using the contact details on the enquiry, issue a one-time claim code (shown once, stored hashed, valid 24 hours, 5 attempts), and the signed-in parent enters reference + code in the parent portal. Only then is parent_account_id set.

Multi-school: published school information (additive)

Every organisation now has versioned school information managed by its school administrator (draft → publish → history). The API returns only the currently published version; drafts and archived versions are never served. GET /api/public/admissions/v1/admissions-info keeps its original fields and gains the optional ones below; GET /api/public/admissions/v1/school-information is an alias with the identical payload. Both require a connector key permitted for the requested organisation_ref (403 forbidden_organisationotherwise) and return 403 forbidden_organisation for a suspended school.

{
  "ok": true,
  "organisation_ref": "demo-academy",
  "school_name": "Demo Academy",
  "logo_url": string | null,
  "workspace_mode": "demonstration" | "live",
  "is_demo": boolean,
  "information_version": 2,                 // integer, increments on every publish
  "published_at": "2026-09-13T17:51:35Z",   // publication timestamp
  "updated_at": "…",                        // unchanged legacy field (= published_at)
  "requirements": [{ "id", "title", "detail" }],        // legacy
  "steps":        [{ "id", "title", "detail" }],        // legacy
  "notes": string | null,                               // legacy
  "programmes": [{ "id", "name", "level", "grades": string[], "description", "duration", "intake" }],
  "fees": [{ "id", "programme", "grade", "fee_type", "currency", "amount", "period",
             "payment_schedule", "effective_from", "notes", "status": "published" }],
  "required_documents": [{ "id", "name", "detail", "mandatory": boolean }],
  "deadlines": [{ "id", "title", "date", "detail" }],
  "contacts": [{ "id", "label", "name", "phone", "email", "role" }],
  "school_hours": [{ "id", "days", "open", "close", "notes" }],
  "faqs": [{ "id", "question", "answer" }],
  "announcements": [{ "id", "title", "body", "published_on", "expires_on" }]
}

Manual pairing of a new school (no automation exists yet)

  1. Platform owner creates the school at /staff/schools (name, logo, contacts, workspace mode). The organisation reference is generated once and never changes.
  2. Copy the exact organisation_ref from the school page into the matching school record in Champ AI Solutions.
  3. On the school page, issue a connector key scoped to that school and store it in the source project's secure settings (never in chat). Add the source website origin(s) to the school's approved hand-off origins.
  4. Grant an administrator: assign an existing signed-in account, or create an explicit invitation link and send it yourself. Nothing is emailed automatically and typing an address grants nothing.
  5. The administrator signs in at /staff, edits /staff/school-information, previews and publishes version 1.
  6. Test from the source project with the school's key (health, information, enquiry, parent sign-in) while the school is still marked Setup, then set it to Active and enable the demo in Champ AI Solutions. Suspended schools are refused everywhere (API, submissions, publishing); Setup is a visible status only.