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.