Zum Inhalt springen

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.

POST https://api.swissmentor.ch/graphql
Content-Type: application/json

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

OperationTypePurpose
publicListingPlatform(guid)QueryReturns one customer-facing platform (theme, footer, language).
publicCourseListings(platformGuid, ...)QueryPaginated catalogue, filterable by topic / level.
publicCourseListing(guid)QuerySingle Kursausschreibung incl. instances and CMS-Block content.
publicCourse(guid)QuerySingle course (Durchführung) detail.
publicCmsBlocksByParent(parentID)QueryPublished CMS-Block content for a parent SMObject (description, prerequisites, learning goals, …). Drafts are never returned.
OperationTypePurpose
publicSurvey(token)QueryAnonymous survey content for a one-time token.
postSurveyAnswers(input)MutationSubmit answers for a token.
OperationTypePurpose
publicEnrollToCourse(input)MutationAnonymous enrollment with embedded OTP/Magic-Link bootstrap. See Public Enrollment for full schema and examples.
OperationTypePurpose
createPublicLead(input)MutationBeratung / Kontakt-Formulare. Creates Person + Lead linked to a configurable OpportunityGroup.
OperationEndpointPurpose
requestEmailLoginPOST /auth/otp-message-requestSends Magic-Link email.
verifyOtpPOST /auth/otp-loginVerifies token, sets JWT cookie.
logoutGET /auth/logoutClears JWT cookie.

The auth-bootstrap flow uses REST endpoints under /auth/... (Express middleware), not GraphQL — see Authentication.

The Public API is open to the internet. The following safeguards apply:

Mutations carry a @PublicRateLimit decorator that throttles per-IP and per-email in a sliding window. Current ceilings:

MutationPer-IPPer-EmailWindow
createPublicLead531 h
publicEnrollToCourse(planned)(planned)—
requestEmailLogin (REST)310 / day1 min

Excess hits return GraphQL error code PUBLIC_RATE_LIMIT_EXCEEDED with a localised user message. Callers should display the message and back off.

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.

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.
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');

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:

CodeTrigger
PUBLIC_RATE_LIMIT_EXCEEDEDPer-IP or per-email throttle exceeded.
INVALID_ARGUMENT_ERRORInput failed class-validator rules (e.g. IsEmail).
OBJECT_NOT_EXISTSReferenced courseListingGuid / opportunityGroupGuid not found.

Internal exceptions surface as generic INTERNAL_SERVER_ERROR — never display the raw message to end users.