Sign in with UniClubs
UniClubs is an OpenID Connect provider. Your site sends students to UniClubs to log in, they approve sharing once, and you get back who they are and (if you ask) whether they’re a member of your club.
Register your app
Under Settings → API & OAuth → OAuth applications, a club admin creates an application with:
- Redirect URIs. As many as you need, including
http://localhost:3000/...for local development. They must match exactly (scheme, host, port, path). - Scopes your app may request (see below).
You get a client_id (unc_…) and a client_secret. The secret is shown once: store it straight away.
Redirect URIs and scopes can’t be changed after creation. To change them, create a new application and switch your site to the new credentials, then delete the old one. Add your localhost URIs up front.
Endpoints
Most OIDC libraries only need the discovery URL:
https://uniclubs.ch/.well-known/openid-configuration| Issuer | https://uniclubs.ch |
| Authorize | https://uniclubs.ch/api/oauth/authorize |
| Token | https://uniclubs.ch/api/oauth/token |
| Userinfo | https://uniclubs.ch/api/oauth/userinfo |
| JWKS | https://uniclubs.ch/api/oauth/.well-known/jwks.json |
| Revoke / introspect | /api/oauth/revoke, /api/oauth/introspect |
The flow
Authorization-code flow, with response_type=code.
- The code exchange needs your client secret (HTTP Basic or
client_secret_post). Do it on your server, never in browser JavaScript. There are no public (secret-less) clients, so a pure single-page app needs a small backend. - PKCE is supported (
S256only) and recommended on top of the secret. - Pass a
nonceand it’s echoed in the ID token. - The ID token is an RS256 JWT, verifiable against the JWKS.
Lifetimes: authorization code 10 min (single use), access token 1 h, ID token 1 h. Request offline_access to get a refresh token (30 days). Refresh tokens rotate on every use: always store the new one. Replaying an old refresh token revokes the whole chain.
Scopes and what you get back
Claims come from the userinfo endpoint. The ID token carries sub, name, email, and picture.
| Scope | Claims |
|---|---|
openid | sub, plus previous_ids when there are older IDs to re-link |
profile | name, given_name, family_name, picture, profile, gender, locale, updated_at |
email | email, email_verified, email_uni, email_private |
user:read | bio, linkedin_url, graduation_year, major, phone, has_cv, current_cv_id, current_cv_url |
membership:read | memberships[], club_role (see below) |
offline_access | a refresh token |
files:write | lets your app upload a CV to the student’s profile (details in the spec ) |
About the email claims: email is the student’s verified private address if they have one, otherwise their university address. email_uni is always the university address and is always verified. email_verified refers to the private address only.
Is this person a member?
Request membership:read. Userinfo then includes the student’s active memberships in your club:
{
"sub": "ucs_3fZq9xT2bKpL0aVw7eRn1Q",
"memberships": [
{
"id": "…",
"club_slug": "consulting-club",
"status": "active",
"tier": "Full Member",
"start_date": "2026-09-01T00:00:00.000Z",
"end_date": "2027-08-31T23:59:59.000Z"
}
],
"club_role": null
}- Not a member, or the membership has expired: the
membershipsandclub_rolefields are left out entirely. Treat a missing field as “not a member”. end_dateis when the membership ends. It’snullfor lifetime memberships.club_roleis the student’s staff role in your club (admin,recruiter,viewer) ornull.
This only answers the question at the moment of sign-in. To check later without the student present (e.g. a nightly sync), use the REST API with the stored sub: GET /api/external/v1/members/{sub} and look for a membership with status: "active".
Keeping IDs stable
sub is your club’s permanent ID for the student. Use it as your key, not the email address, since students can change their emails.
When a student merges two duplicate UniClubs accounts, you keep seeing the ID you already stored. The only exception is if your club knew both accounts: you then get one of the two IDs as sub, and the other in previous_ids:
{ "sub": "ucs_3fZq…", "previous_ids": ["ucs_Hk81…"] }Handle previous_ids at sign-in: if you don’t recognise sub, look for a local user stored under one of the previous_ids and switch them to the new sub. The claim is in both userinfo and the ID token, and is left out when empty. Old IDs also keep working in the REST API, which always answers with the current ID.
Switch to club IDs on 11 November 2026. Clubs that integrated before that date currently receive global UniClubs IDs. On 11 November every club switches to club-scoped IDs: sub becomes a ucs_… ID and the old global ID shows up in previous_ids. If you handle previous_ids as described above, your users are re-linked automatically on their next sign-in. Tokens from an existing sign-in (including refreshed ones) keep the ID they started with until then. Old IDs keep resolving in the REST API, so background syncs keep working in the meantime.
Revoking access
Students can see and revoke connected apps in their UniClubs settings. A revoked app’s refresh tokens stop working, and the student sees the consent screen again on next sign-in.