Stud API — Integration Guide
Overview
The Stud API lets you verify that a user is affiliated with an academic institution through their institutional single sign-on (SAML 2.0). Instead of building SAML yourself — handling certificates, metadata, and dozens of federation protocols — you call two simple REST endpoints.
Institutions covered: Universities, research labs, and academic federations worldwide via eduGAIN (70+ national federations including DFN-AAI [Germany], RENATER [France], InCommon [US/Canada], SWITCHaai [Switzerland], Edugate [Ireland], HAKA [Finland], and more).
What you get: A verified institutional login. The API computes an authIdentifier you can use for user accounts, or tells you when no persistent identifier is available. You also get the institution's identity (entityId) and optionally their affiliations (student, faculty, etc.).
Institutional Coverage
The Stud API is backed by eduGAIN, the global inter-federation service that connects over 70 national research and education identity federations. When a user authenticates, they do so through their home institution's identity provider (IdP), which is registered in one of these federations.
Federations covered through eduGAIN:
| Federation | Country / Region |
|---|---|
| DFN-AAI | Germany |
| InCommon | United States, Canada |
| UK Access Management Federation | United Kingdom |
| RENATER | France |
| SWITCHaai | Switzerland |
| SURFconext | Netherlands |
| HAKA | Finland |
| Edugate (HEAnet) | Ireland |
| GakuNin | Japan |
| ...and 60+ more | Worldwide |
Not covered: Institutions that are not members of an eduGAIN-participating national federation. This is typical in regions without mature research and education identity infrastructure. For these institutions, the API cannot reach them.
The "single instance, many institutions" model used by Stud is a proven architectural pattern in academic federations. For example, RENATER (France) successfully connects a single on-premises Nextcloud instance to the IdPs of hundreds of French organizations using the same SAML brokering approach.
How SAML Verification Works
The flow has three steps:
Your App Stud API User's Institution
│ │ │
│── 1. POST /verification─→ │
│←── { id, link } ───────│ │
│ │ │
│── 2. Redirect user ────→│ (selects institution) │
│ │── SAML Auth Request ──────→
│ │←── SAML Assertion ───────│
│ │ │
│── 3. GET /verification/:id ──→ │
│←── { session: {...} } ──│ │
│ │ │
Create a verification session — your backend calls
POST /v2/auth/verificationwith a secret token, your callback URL, and a service name. You get back a unique verification ID and a link.User authenticates with their institution — you redirect the user to the link. The Stud web app guides them through selecting their institution and completing the SAML login. The user completes any MFA required by their institution (e.g., Microsoft Authenticator, Duo, FIDO2 passkey) — you don't handle credentials or MFA.
Check the result — after the user returns to your callback URL, your backend calls
GET /v2/auth/verification/:idto get the session data.
Step by Step
Step 1: Create a Verification Session
POST /v2/auth/verification
Send a JSON body with:
| Field | Type | Required | Description |
|---|---|---|---|
secretToken |
string | yes | A secret your backend generates. Use it to authenticate your status-check request later. 1–256 characters. |
redirectUrl |
string | yes | Where the user should land after completing SAML login at their institution. Must be a valid URL, max 2048 characters. |
serviceName |
string | yes | Human-readable name of your service (shown to the user during the flow). 1–256 characters. |
Response 201 Created:
{
"id": "a1b2c3d4-...",
"link": "https://studid.io/search?id=a1b2c3d4-...&serviceName=Your%20Service"
}
The link is a pre-built URL to the Stud web app. Redirect the user to this link.
Verification lifetime: Verifications expire after 60 minutes. The SAML authorization link within the verification is valid for 30 minutes — the user must complete their institutional login within that window. If the verification expires before a SAML login completes, create a new verification.
Step 2: User Authenticates
The user sees the Stud web app, picks their institution from the searchable list, and completes the SAML login at their institution's identity provider. If the institution requires MFA, the user completes it on the institution's side — no impact on your integration.
You don't need to do anything during this step — the SAML exchange happens between the Stud infrastructure and the institution.
After completion, the user is redirected to your redirectUrl with a ?verificationId=... query parameter.
Step 3: Check Verification Status
GET /v2/auth/verification/:id
Query parameters:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (UUID) | yes | The verification ID from step 1. |
secretToken |
string | yes | The same secret token you provided in step 1. |
Response 200 OK:
{
"id": "a1b2c3d4-...",
"created": "2026-05-03T10:30:00.000Z",
"session": {
"authIdentifier": "MCE6NXEQ3FC3PU...@tu-berlin.de",
"authIdentifierType": "pairwise-id",
"entityId": "https://idp.tu-berlin.de/shibboleth",
"affiliations": ["student", "member"]
}
}
If no SAML login has completed yet, session will be null. Poll until you see data (2–5 second intervals recommended).
User Identifiers
The session response provides several identifier fields. The API computes the most usable one for you — this is the single field you should read for authorization.
authIdentifier — Use This for User Accounts
authIdentifier is the field to use for linking users to accounts in your system. It is computed server-side with the following logic:
| Priority | Condition | authIdentifierType |
Reliable? |
|---|---|---|---|
| 1 | pairwiseId is present |
"pairwise-id" |
Best — modern per-SP pseudonym, international standard |
| 2 | nameIdFormat is persistent |
"persistent-nameid" |
Good — older standard, still widely used, same user always gets same ID |
| 3 | nameIdFormat is emailAddress |
"email" |
OK — verified institutional email, but emails can be reassigned |
| 4 | None of the above | null |
Not available — the IdP returned a transient (random) identifier |
Examples:
// Best case: modern IdP supports pairwise-id
{ "authIdentifier": "MCE6NXEQ3FC3...@tu-berlin.de", "authIdentifierType": "pairwise-id" }
// Common case: persistent NameID, no pairwise-id
{ "authIdentifier": "hugo123@uni-musterstadt.de", "authIdentifierType": "persistent-nameid" }
// Email-based
{ "authIdentifier": "user@university.edu", "authIdentifierType": "email" }
// Transient — no usable identifier
{ "authIdentifier": null, "authIdentifierType": null }
authIdentifier: null — What to Do
When authIdentifier is null, the institution sent a transient (random) identifier that changes every login. You cannot use it for persistent user accounts. However, you still have:
entityId— conclusively identifies which institution the user authenticated at. This is always available.nameId— the raw SAML identifier. ChecknameIdFormatto understand its type.affiliations— if released, tells you their role.
Recommendation: Use per-session tokens or cookies for users with transient identifiers. Track entityId to know which institution they're from.
entityId — The Always-Available Fallback
entityId is the SAML entity ID of the institution's identity provider. It uniquely identifies the institution (e.g., https://idp.tu-berlin.de/shibboleth). This field is always present and confirms that the user successfully authenticated at that institution — even when no persistent user identifier is available.
Background: Why Identifiers Vary
The difference between getting a persistent ID and getting a transient one depends on:
- The IdP's SAML configuration — some institutions ignore the requested NameID format
- Entity categories — the API is not a member of the REFEDS Research & Scholarship (R&S) category, so IdPs are not obligated to release specific identifiers
- Federation policies — each national federation (DFN-AAI, InCommon, SWITCHaai, etc.) has different defaults
This is normal SAML federation behavior, not a Stud-specific limitation. authIdentifier abstracts this complexity for you.
Understanding Affiliations
The affiliations array in the response contains values from the eduPerson standard (REFEDS). These describe the user's relationship to their institution in broad categories.
Important: As a non-R&S service, the API does not automatically receive affiliation data. Most IdPs will return an empty affiliations: []. This is normal and expected. When affiliations ARE present, use them. When they're not, trust authIdentifier and entityId as your proof of institutional affiliation.
Standard Values
| Value | Meaning |
|---|---|
faculty |
Teaching or research faculty |
student |
Enrolled student |
staff |
Administrative or support staff |
employee |
Employed by the institution (country-dependent — see caveats below) |
member |
Full member of the university community (umbrella value — should be asserted for faculty, staff, student, employee) |
alum |
Former student (does NOT imply member) |
affiliate |
Some affiliation not covered by other values (volunteers, parents, guests — unreliable across institutions) |
library-walk-in |
Physical access to library facilities only |
The member Value
member is the most reliable integration signal. By the REFEDS standard, institutions MUST assert member for any person carrying one or more of:
facultystaffstudentemployee
In practice, member means "this person has the full set of basic institutional privileges."
The alum Distinction
People with the alum affiliation are typically not members — they are former students no longer eligible for the same privileges as current faculty, staff, or students.
Caveats and Real-World Issues
employee vs staff vs faculty are unreliable internationally. Different countries define these differently. In some countries, employee includes all paid staff including faculty. In others, employee is a separate category from staff. These three values should not be used as the sole basis for access decisions.
Most IdPs don't release affiliations to non-R&S services. An empty affiliations: [] array is the common case. It does not mean the user isn't a student — it means the IdP chose not to release that attribute.
Some institutions don't assert member. While required by the REFEDS standard, some institutions omit it.
Custom or missing affiliation values. A few institutions use non-standard values or assert no affiliations. The API returns whatever the IdP provides — we don't filter.
Recommendations
- Don't rely on affiliations for core auth. Use
authIdentifierorentityIdas your primary identity proof. Treat affiliations as enrichment. - When affiliations are present, prefer
member— it's the most standard value. - Layer specific checks on top — after checking
member, check forfaculty,student,staffto refine access. - Don't make hard decisions on
employee/staffalone — they're country-dependent. - Treat empty affiliations as valid but indeterminate — the user authenticated successfully, role data just isn't available.
Privacy & Data Minimization
The SAML-based verification model used by the Stud API is inherently privacy-preserving compared to document-based verification services.
What You Never See
- User passwords — the user authenticates at their institution, not with you
- Government IDs, transcripts, or enrollment documents — no document uploads needed
- Raw SAML XML — the API abstracts the protocol
What You Receive
- A computed
authIdentifier(ornull) - The institution's identity (
entityId) - Optionally, affiliations and the raw SAML NameID
GDPR and Data Minimization
The federated SSO model is inherently compliant with the GDPR principle of data minimization. By receiving a digitally signed assertion from the university, you avoid collecting and storing high-risk personal documents. The institution controls which attributes are released through their Attribute Release Policy.
MFA Handling
If the user's institution requires multi-factor authentication (Microsoft Entra ID, Duo, FIDO2 passkeys, etc.), the user completes it on the institution's side. This is transparent to your integration — the SAML assertion is only returned after all institutional authentication requirements are satisfied.
Age Restrictions
For legal compliance across jurisdictions (GDPR, CCPA, COPPA), we recommend a minimum age of 16+ for users of your service.
Rate Limits
The API enforces rate limits to protect the service:
| Scope | Limit |
|---|---|
| Global | 100 requests per 60 seconds per IP |
Auth endpoints (/v2/auth/*) |
20 requests per 60 seconds per IP |
Response 429 Too Many Requests:
{
"error": "RateLimitError",
"message": "Too many requests"
}
If you hit a rate limit, wait and retry. For polling verification status, we recommend a 2–5 second interval.
Error Handling
| Status | When |
|---|---|
201 |
Verification created successfully |
200 |
Verification status retrieved |
400 |
Invalid request data (missing fields, invalid UUID, etc.) |
404 |
Resource not found (route doesn't exist) |
429 |
Rate limit exceeded |
500 |
Internal server error |
| Verification expired | Check fails with "not found" — verification TTL is 60 minutes |
| SAML request expired | Authorization link expired after 30 minutes — create a new verification |
Validation errors (400) include details about which fields failed:
{
"error": "Validation Error",
"message": "Invalid request data",
"details": [
{
"code": "too_small",
"path": ["secretToken"],
"message": "Too small: expected string to have >=1 characters"
}
]
}
FAQ / Troubleshooting
Why is authIdentifier null?
The institution returned a transient (random per login) NameID. This happens when the IdP doesn't honor the requested persistent format, or doesn't support the pairwise-id attribute. Use entityId (which institution) and per-session tracking instead.
Why are affiliations empty?
Most institutions do not release eduPersonScopedAffiliation to services that are not in the REFEDS Research & Scholarship (R&S) entity category. This is normal SAML federation behavior. Empty affiliations do not mean authentication failed — check authIdentifier and entityId.
How long does a verification last?
Verifications expire 60 minutes after creation. If the SAML login hasn't completed within 30 minutes of creating the verification, the authorization link becomes invalid. In either case, create a new verification and redirect the user again. Expired verifications are automatically cleaned from Redis — no manual cleanup needed.
Which institutions are covered?
Any institution whose identity provider is registered in an eduGAIN-participating national federation (70+ federations worldwide). Use the entity search endpoint or the Stud web app to check if a specific institution is available.
What if my target institution is not covered?
The institution needs to be a member of a national research and education identity federation that participates in eduGAIN. Until then, the API cannot reach them.
Does MFA break the login flow?
No. The user completes any required MFA (Microsoft Authenticator, Duo, FIDO2, etc.) at their institution's login page. This is transparent to your integration.
Can I use nameId directly instead of authIdentifier?
You can, but you must check nameIdFormat first. If it's transient, the value changes every login and is worthless for authorization. If it's persistent or emailAddress, it's safe. authIdentifier does this check for you.
The session data changed since my last integration. Is this backward-compatible?
Yes. New fields (authIdentifier, authIdentifierType, nameIdFormat, pairwiseId) are additive. Existing fields (nameId, entityId, affiliations) remain unchanged.
Code Example (JavaScript)
const API_BASE = 'https://api.studid.io'
async function createVerification(secretToken, redirectUrl, serviceName) {
const res = await fetch(`${API_BASE}/v2/auth/verification`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ secretToken, redirectUrl, serviceName }),
})
if (!res.ok) {
const err = await res.json()
throw new Error(`Verification creation failed: ${err.message}`)
}
return res.json() // { id, link }
}
async function checkVerification(id, secretToken) {
const params = new URLSearchParams({ id, secretToken })
const res = await fetch(`${API_BASE}/v2/auth/verification/${id}?${params}`)
if (!res.ok) {
const err = await res.json()
throw new Error(`Verification check failed: ${err.message}`)
}
return res.json() // { id, created, session }
const status = await checkVerification(id, secretToken)
if (status.session && status.session.authIdentifier) {
console.log(`User ID: ${status.session.authIdentifier} (${status.session.authIdentifierType})`)
await findOrCreateUser(status.session.authIdentifier, status.session.entityId)
} else {
// Transient identifier — use per-session tracking
console.log(`No persistent ID — user authenticated at ${status.session?.entityId}`)
createSessionToken(status.session?.entityId)
}
API Reference
For the interactive API playground and full schema details, see the API Reference.