Public API
Public API
Abschnitt betitelt „Public API“The Public API is a curated subset of the SwissMentor GraphQL schema that requires no authentication. Customer-facing Astro websites, embeddable Web-Components and signup flows use it to render course catalogues, push course enrollments, and create CRM Leads from contact forms.
All Public-API operations live behind the resolver decorator @Authorized(auth('Public')). They never expose internal IDs (only UID GUIDs), never return ACL-restricted data, and never accept staff-only inputs.
Endpoint and Headers
Abschnitt betitelt „Endpoint and Headers“POST https://api.swissmentor.ch/graphqlContent-Type: application/jsonNo Authorization header. Cookies are optional — only the OTP/Magic-Link flow sets SM_JWT_TOKEN once a user authenticates from a public flow. Cross-origin callers should send credentials: 'include' so cookies survive the round-trip.
Available Operations
Abschnitt betitelt „Available Operations“Listing Platform & Catalogues
Abschnitt betitelt „Listing Platform & Catalogues“| Operation | Type | Purpose |
|---|---|---|
publicListingPlatform(guid) | Query | Returns one customer-facing platform (theme, footer, language). |
publicCourseListings(platformGuid, ...) | Query | Paginated catalogue, filterable by topic / level. |
publicCourseListing(guid) | Query | Single Kursausschreibung incl. instances and CMS-Block content. |
publicCourse(guid) | Query | Single course (Durchführung) detail. |
publicCmsBlocksByParent(parentID) | Query | Published CMS-Block content for a parent SMObject (description, prerequisites, learning goals, …). Drafts are never returned. |
Surveys
Abschnitt betitelt „Surveys“| Operation | Type | Purpose |
|---|---|---|
publicSurvey(token) | Query | Anonymous survey content for a one-time token. |
postSurveyAnswers(input) | Mutation | Submit answers for a token. |
Enrollment
Abschnitt betitelt „Enrollment“| Operation | Type | Purpose |
|---|---|---|
publicEnrollToCourse(input) | Mutation | Anonymous enrollment with embedded OTP/Magic-Link bootstrap. See Public Enrollment for full schema and examples. |
CRM / Contact
Abschnitt betitelt „CRM / Contact“| Operation | Type | Purpose |
|---|---|---|
createPublicLead(input) | Mutation | Beratung / Kontakt-Formulare. Creates Person + Lead linked to a configurable OpportunityGroup. |
Auth Bootstrap
Abschnitt betitelt „Auth Bootstrap“| Operation | Endpoint | Purpose |
|---|---|---|
requestEmailLogin | POST /auth/otp-message-request | Sends Magic-Link email. |
verifyOtp | POST /auth/otp-login | Verifies token, sets JWT cookie. |
logout | GET /auth/logout | Clears JWT cookie. |
The auth-bootstrap flow uses REST endpoints under /auth/... (Express middleware), not GraphQL — see Authentication.
Anti-Abuse Measures
Abschnitt betitelt „Anti-Abuse Measures“The Public API is open to the internet. The following safeguards apply:
Rate Limits
Abschnitt betitelt „Rate Limits“Mutations carry a @PublicRateLimit decorator that throttles per-IP and per-email in a sliding window. Current ceilings:
| Mutation | Per-IP | Per-Email | Window |
|---|---|---|---|
createPublicLead | 5 | 3 | 1 h |
publicEnrollToCourse | (planned) | (planned) | — |
requestEmailLogin (REST) | 3 | 10 / day | 1 min |
Excess hits return GraphQL error code PUBLIC_RATE_LIMIT_EXCEEDED with a localised user message. Callers should display the message and back off.
Honeypot Field
Abschnitt betitelt „Honeypot Field“createPublicLead defines an optional website input field. Frontends render it CSS-hidden (off-screen, aria-hidden, tabindex="-1"). Real users never fill it. When the server receives a non-empty value, it returns the same generic success response as a legitimate submission so scrapers cannot detect rejection.
Apply the same pattern to any new public form.
Future Hardening (Phase 2)
Abschnitt betitelt „Future Hardening (Phase 2)“Tracked in the platform plan, not yet shipped:
- Cloudflare Turnstile token verification on heavy mutations.
- Dynamic CORS allowlist sourced from
CmsCollection.prodDomain(today: hardcoded localhost ports). - GraphQL query-complexity / depth limits for
auth('Public')operations. - Loki-based anomaly alerting on per-endpoint request rate.
Example: Create a Lead from a Contact Form
Abschnitt betitelt „Example: Create a Lead from a Contact Form“const mutation = ` mutation CreatePublicLead($input: PublicLeadInput!) { createPublicLead(input: $input) { success message } }`;
const res = await fetch(apiBase + '/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ query: mutation, variables: { input: { prename: 'Anna', lastname: 'Muster', email: 'anna@example.ch', phone: '+41 79 555 12 34', message: 'Bitte um Beratung zum HF-Studiengang.', source: 'kontakt-page', courseListingGuid: 'AB12-...', website: '', // honeypot — leave empty }, }, }),});
const json = await res.json();const result = json?.data?.createPublicLead;if (result?.success) showSuccess(result.message);else showError(result?.message ?? 'Unbekannter Fehler');Example: Fetch a Course Catalogue at Build Time
Abschnitt betitelt „Example: Fetch a Course Catalogue at Build Time“Astro static-build calls the Public API from Node:
const query = ` query PublicCourseListings($platformGuid: String!) { publicCourseListings(platformGuid: $platformGuid) { items { id name slug descriptionShort imageUrl } } }`;const data = await graphql(query, { platformGuid: SCS_PLATFORM_GUID });The catalogue is regenerated on every release and on the cron content-refresh (every 2 h) — see the SCS pipeline.
All errors arrive as standard GraphQL errors[]. User-relevant errors extend BaseUserError and carry an extensions.code field — frontends should branch on the code, not on the message text:
| Code | Trigger |
|---|---|
PUBLIC_RATE_LIMIT_EXCEEDED | Per-IP or per-email throttle exceeded. |
INVALID_ARGUMENT_ERROR | Input failed class-validator rules (e.g. IsEmail). |
OBJECT_NOT_EXISTS | Referenced courseListingGuid / opportunityGroupGuid not found. |
Internal exceptions surface as generic INTERNAL_SERVER_ERROR — never display the raw message to end users.