Skip to content
API documentation
View as MarkdownOpen in Claude

Guides

Students

Almost every integration starts with a student. You register the student once, keep your own record of the _id the API gives back, and use that id everywhere afterwards: to send the student into the panel, to enter them for exams, and to collect their certificates and reports.

"Your students" means the students your API account registered. You can read and change those and no others. A student registered by another account behaves exactly as if it did not exist, and so does a student on your old account if an operator issues you a new one.

The operations

OperationRoutePermissionActs on
Register a studentPOST /v1/studentstudent/create on mtothe core record
Check a registrationPOST /v1/student/checkstudent/create on mtonothing: it writes nothing
List your studentsGET /v1/studentstudent/read on mtothe core record
Get a studentGET /v1/student/<studentId>student/read on mtothe core record
Update a studentPUT /v1/student/<studentId>student/update on mtothe core record
List an organization's studentsGET /v1/<organizationId>/studentstudent/read on that organizationthe core record, filtered
Get a student on an organizationGET /v1/<organizationId>/student/<studentId>student/read on that organizationthe core record, filtered
Update a student on an organizationPUT /v1/<organizationId>/student/<studentId>student/update on that organizationthe core record

A role grants a permission "on mto" when its target is mto or *, and "on that organization" when its target is that organization's slug or * (Permissions). Three more student operations have their own guides: passwords, supervisor links and, for a class at a time, bulk registration.

Note

Registering a roster? registerStudent takes one student per request. For 30 to 1000 of them, createStudentImport takes the whole list in one request, checks it before it accepts it, and registers all of them or none. See Bulk registration.

The core record and organization copies

A student exists in more than one place, and knowing which one an operation touches explains most of the rules on this page.

The core record is created when you register the student. It lives on mto, the core organization, and holds the student's profile, username and password. Every student operation on this page reads or writes the core record, including the ones with an <organizationId> in the path. The _id you get at registration is the core record's id, and it is the <studentId> every student route takes.

An organization copy is the record one organization (stem, hilingua, neo, gmath or coding) keeps of the same student. It is created the first time the student signs in to that organization, through a sign-in link you create or on their own. The API cannot create one any other way. A copy has its own _id, different on every organization, and carries the core record's _id in mainId.

Core recordOrganization copy
CreatedWhen you call POST /v1/studentAt the student's first sign-in to that organization
Id_id: the id you store and pass as <studentId>Its own _id on that organization; mainId = the core _id
Written by the student routesYesNo
Needed forEverything on this pageApplications, supervisor links, and the student's certificates and reports on that organization
Kept in stepProfile details are copied from the core record each time the student signs in there

What this means in practice:

  • A new student has no copies. Until the student has signed in to an organization, routes that need the copy refuse. For example, creating an application answers 409 with a message saying the student has never signed in to that organization. The fix is always the same: create a sign-in link for that organization and have the student open it.
  • Your changes reach the checks this API makes at once. Exam eligibility, for example, is judged from the core record's grade and country. An organization's own panel shows the student as its copy holds them, which picks up your change the next time the student signs in there.
  • Match on mainId when a record comes from an organization. An application's user, or the student in a supervisor-link response, is the organization copy. Its _id means nothing elsewhere, but its mainId is the id you know. Identifiers covers this in detail.

Which organizations a student is entered on

activatedPlatformsThisSeason lists the organizations the student is entered on this season:

ValueMeaning
"common"Every organization. This is the default when you register a student without the field.
"stem", "hilingua", "neo", "gmath", "coding"That organization.

The values are lower case. Anything else is refused with 400 and each value in activatedPlatformsThisSeason must be one of the following values: common, stem, hilingua, neo, gmath, coding, and that includes "mto": every student is on the core record already. Several values can be combined: ["stem", "neo"]. At registration an empty list, [], is accepted and enters the student on no organization: the organization routes below leave them out, and sign-in links are refused with 403 until you add one.

The list decides which organization routes treat the student as yours:

  • GET /v1/<organizationId>/student and GET /v1/<organizationId>/student/<studentId> only return students whose list contains "common" or that organization's slug.
  • A sign-in link for an organization the student is not entered on is refused with 403 and Student is not activated for organization <slug>.
  • The organization-wide application listings (GET /v1/<organizationId>/application and GET /v1/<organizationId>/application/exam-applications/<examId>) only include students entered on that organization.

Adding organizations

The list only ever grows. Both updates add to it, and no operation removes an organization from it:

You callWith activatedPlatformsThisSeason in the bodyWithout it
PUT /v1/student/<studentId>The values you send are added to the list.Unchanged.
PUT /v1/<organizationId>/student/<studentId>The values you send are added to the list. The organization in the path is added only if you name it.That organization is added, unless the list already contains "common".
  • A value the list already holds is not added twice, so sending the same list again changes nothing.
  • [] adds nothing.
  • null is refused with 400 and activatedPlatformsThisSeason must be an array, as it is at registration.
  • A slug added to a list that holds "common" is stored beside it. That changes nothing, because "common" already covers every organization.

To add one organization, send just its slug, or send that organization's update route an empty body (below). You never need to read the current list first.

Register a student

POST /v1/student creates the core record and answers 201 with the new student.

curl -X POST "https://api.main-team.org/v1/student" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Ada",
    "lastName": "Lovelace",
    "email": "ada.lovelace@example.org",
    "birth": "14/05/2010",
    "sex": "f",
    "country": "630e0182c53dc79a6836e67e",
    "grade": "9",
    "city": "Berlin",
    "school": "Berlin International School",
    "phone": "+49 30 123 4567"
  }'
{
  "success": true,
  "message": "User registered successfully.",
  "data": {
    "_id": "6650f1a2b3c4d5e6f7a8b9c0",
    "username": "XXB1045",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "fullName": "Ada Lovelace",
    "email": "ada.lovelace@example.org",
    "emailConfirmed": false,
    "phone": "+49 30 123 4567",
    "birth": "14/05/2010",
    "sex": "f",
    "country": "630e0182c53dc79a6836e67e",
    "city": "6650e0a1b2c3d4e5f6a7b801",
    "school": "6650e0a1b2c3d4e5f6a7b8c2",
    "grade": "630e01826836e67ec53dc7a5",
    "activatedPlatformsThisSeason": ["common"],
    "createdAt": "2026-09-15T09:12:44.512Z",
    "updatedAt": "2026-09-15T09:12:44.512Z"
  }
}

Store data._id. It is the student's id on every route from now on. Note that city, school and grade come back as ids even though the request named them: the API resolves every reference to the stored id before it writes anything.

Fields

FieldTypeRequiredFormat and rulesExample
firstNamestringyesNot empty."Ada"
lastNamestringyesNot empty. Without it the registration is refused with 400 and lastName should not be empty."Lovelace"
emailstringyesA valid email address, with no spaces around it (" ada@example.org" is refused as not an address). Stored in lower case. Must not belong to any other student on the platform (Duplicates)."ada.lovelace@example.org"
birthstringyesDD/MM/YYYY: two-digit day 01–31, two-digit month 01–12, four-digit year, and a date that exists on the calendar. 31/02/2010 and ISO dates such as 2010-05-14 are refused with birth must be a real date in DD/MM/YYYY format, at registration and on both updates."14/05/2010"
sexstringyesExactly "m", "f" or "n", in lower case."f"
countrystringyesA country _id from GET /v1/country. An id only: names and ISO codes are refused."630e0182c53dc79a6836e67e"
gradestringyesA grade _id from GET /v1/grade, or the grade's name. Always a string: the number 9 is refused with grade must be a string."9"
citystringyesA city's _id, or its name within country."Berlin"
schoolstringyesA school's _id, or its name within country and city."Berlin International School"
phonestringnoAny string; not validated. null is refused with phone must be a string: leave the field out instead. International format is the most useful to everyone who reads it."+49 30 123 4567"
activatedPlatformsThisSeasonarray of stringsno"common" or organization slugs (above). Defaults to ["common"]. null is refused with activatedPlatformsThisSeason must be an array: leave the field out instead.["common"]
passwordstringnoOnly from an account that also holds auth/signin on mto; 5 characters to 72 bytes; see Passwords. Never returned."grapefruit lantern quarry"

Leave email2 out. It exists to catch automated form spam, and a registration that fills it in is refused.

Every other property is refused with 400 and property <name> should not exist, naming it. That includes fields a student record has but you may not set: username, fullName, emailConfirmed, supervisor, mainId, userType and roles. The API sets those itself, or they belong to the student and the platform.

fullName is derived from firstName and lastName ("Ada Lovelace"), and kept in step when either changes. Certificates and reports print it.

What happens, in order

Each step runs only if the one before it passed, and no student is created unless every step passes.

  1. Your token and permission. 401 or 403 (Insufficient role permissions).
  2. The body. 400 for a missing or malformed field, including a birth date that does not exist, or a property the API does not accept.
  3. The password permission. If the body has a password, the account must also hold auth/signin on mto, or the answer is 403.
  4. A duplicate on your account. If one of your students already has this email: 409, with that student's id in the message.
  5. Reference fields. country, grade, city and school are resolved to stored ids, or the answer is 400 naming the field (below).
  6. The username is created (Usernames).
  7. The write. 409 if the email belongs to a student on another account.

When a body breaks several rules, the error message names one of them. Fix it and send again. Checking a registration first runs steps 2, 4 and 5, and the part of step 6 that needs the country, without creating anyone, and names every reference that fails.

Handling the answers

const BASE = 'https://api.main-team.org/v1';

export async function registerStudent(token, student) {
  const res = await fetch(`${BASE}/student`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(student),
  });
  const body = await res.json();
  if (res.status === 201) return { created: true, student: body.data };

  const { code, message, request_id } = body.error;
  if (res.status === 409) {
    // Already one of yours: the message ends with "(<studentId>)".
    const mine = message.match(/\(([0-9a-f]{24})\)\.$/);
    if (mine) return { created: false, existingId: mine[1] };
    // Held by a student on another account: this address cannot be used.
    throw new Error(`Email in use elsewhere: ${student.email} (request ${request_id})`);
  }
  throw new Error(`${res.status} ${code}: ${message} (request ${request_id})`);
}
<?php
const MT_BASE = 'https://api.main-team.org/v1';

function mt_register_student(string $token, array $student): array
{
    $ch = curl_init(MT_BASE . '/student');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode($student),
        CURLOPT_TIMEOUT => 30,
    ]);
    $raw = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $body = json_decode($raw, true);

    if ($status === 201) {
        return ['created' => true, 'student' => $body['data']];
    }
    $error = $body['error'];
    if ($status === 409) {
        // Already one of yours: the message ends with "(<studentId>)".
        if (preg_match('/\(([0-9a-f]{24})\)\.$/', $error['message'], $m)) {
            return ['created' => false, 'existingId' => $m[1]];
        }
        throw new RuntimeException("Email in use elsewhere: {$student['email']} (request {$error['request_id']})");
    }
    throw new RuntimeException("$status {$error['code']}: {$error['message']} (request {$error['request_id']})");
}

Check a registration first

POST /v1/student/check takes the body you would send to POST /v1/student, without password, and runs the checks registration makes before it creates anything: the body's rules, a duplicate among your students, and the four reference fields. It creates nothing, issues no username, and can be sent any number of times. It needs the permission registration needs, student/create on mto. Use it to validate a batch, or a form, before you register anyone.

curl -X POST "https://api.main-team.org/v1/student/check" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Ada",
    "lastName": "Lovelace",
    "email": "ada.lovelace@example.org",
    "birth": "14/05/2010",
    "sex": "f",
    "country": "630e0182c53dc79a6836e67e",
    "grade": "9",
    "city": "Berlin",
    "school": "Berlin Harbour School"
  }'
{
  "success": true,
  "message": "Registration checked. Nothing was created.",
  "data": {
    "valid": false,
    "problems": [
      {
        "field": "school",
        "message": "school is not a known school. This API does not create reference data."
      }
    ],
    "resolved": {
      "country": "630e0182c53dc79a6836e67e",
      "grade": "630e01826836e67ec53dc7a5",
      "city": "6650e0a1b2c3d4e5f6a7b801",
      "school": null
    },
    "duplicate": { "sameAccount": false }
  }
}
FieldMeaning
validtrue when POST /v1/student with the same body would pass every check made here.
problemsEvery reference field that matches nothing, as { field, message }, with the message registration would refuse it with. Registration names only the first; the check names them all, in the order country, grade, city, school. A city or school named inside a country or city that matched nothing is not looked up, so fix the field above it first. A country whose students cannot be given a username is listed as well.
resolvedThe _id each reference resolved to, which is what registration would store, or null.
duplicatesameAccount is true when one of your students already has this email address, and studentId is that student's _id: registration would answer 409. Nothing else is looked up then, so problems is empty and every resolved value is null.

The answer is 200 whatever the lookups found. A body that breaks a field's rule is refused with 400, exactly as registration refuses it, and so is password, with property password should not exist.

Note

valid: true is not a promise. Only your own students are checked for the email address. An address a student on another account holds is not looked for, so registration can still answer 409 with That email address is already registered. The check also cannot see a student registered, or reference data changed, after it ran.

How reference fields resolve

country, grade, city and school point at the platform's reference data. Whatever form you send, the API looks it up and stores the matching record's id. A value that matches nothing is refused: the API never invents or creates reference data.

FieldAcceptsA name is looked upRefused with 400 when
countryan id only(names are not accepted)not an id: country must be a mongodb id; an id that matches nothing, or a country that cannot be selected: country is not a known country.
gradean id or a nameamong all gradesno grade has that id: grade is not a known grade.; or that name: grade is not a known grade. This API does not create reference data.
cityan id or a namewithin the student's countryno match: city is not a known city. (id) or city is not a known city. This API does not create reference data. (name)
schoolan id or a namewithin the student's country and cityno match: school is not a known school. (id) or school is not a known school. This API does not create reference data. (name)

The details:

  • Id or name is decided per value. A 24-character hex string is treated as an id and looked up as one. Anything else is treated as a name. You can mix the forms freely, for example a country id with a city name.
  • Names ignore case and surrounding spaces. " tirana " finds TIRANA. Reference names are stored in upper case, which is how they come back in reads.
  • Names are looked up inside the fields above them, in the order country, city, school. A city named Springfield is only looked for in the student's country, and a school in that country and city. An exact match always wins. Failing that, a city that is tied to no country matches, and a school in the right country that is tied to no city matches. Some older entries are stored that way.
  • An id is only checked for existence. A city or school given by id is accepted if that record exists, whether or not it lies in the student's country. When you change a student's country, send city and school in the same request so the three still agree.
  • Some countries cannot be selected. GET /v1/country does not list them, and a write that names one is refused exactly like an unknown id.
  • There is no list of cities or schools. Use the names your own records hold. If the platform does not know a real city or school, the API cannot add it: write to info@main-team.org.
  • On an update, a city or school name is resolved against the country and city in the same body or, when you leave them out, against the ones the student already has. You can change the school without repeating the country.

Note

Why the grade is strict. Every exam is open to a set of grades, and grade ids are the same on every organization, so a student's grade is compared directly with the exam's. A grade the platform does not know would be a student no exam could ever accept, so an unknown grade is refused, never created.

What the API never creates

  • Reference data. No country, grade, city or school is ever created, whatever you send.
  • Users other than students. Supervisors, parents and staff register through the platform. The API manages students and nothing else.
  • Organization copies. Only the student's first sign-in to an organization creates one (above).
  • Usernames of your choosing. The API mints every username; you cannot set or change one.
  • Email confirmation. The API never marks an address as confirmed. A student confirms their own address in the panel, with a 6-digit code emailed to them, and the panel asks for it on every page until they do. See Email confirmation.
  • Messages to the student. Registering a student through the API does not email them. Tell them yourself how they will get in; usually that is a sign-in link from your own product.

Usernames

Every student gets a username when they are registered, and it is in every student response as username. It is the identifier students and support staff use to refer to an account.

The format is the country's two-letter code, one letter, then a number. In XXB1045, XX stands for the country's code, B is the letter and 1045 the number. These pages write XX, which is no country's code, so that no example is a real student's username.

  • The letter is chosen at random from A to Z, leaving out P, S and T.
  • The number counts up per country and letter, starting at 1000, so usernames are unique within that series.
  • Usernames are upper case, and read-only. Sending username in any body is refused with 400, property username should not exist.
  • A username never changes through the API, not even when you change the student's country.

Because a username identifies the student to other people, it is not a secret. Never use it as, or in, a password. The API refuses passwords that contain it (Passwords).

Duplicate email addresses

An email address belongs to one student on the whole platform. Two refusals follow from that, and they differ in whether they tell you who holds the address:

SituationStatusMessage
One of your students already has the address409 conflictA student with that email is already registered to this account (6650f1a2b3c4d5e6f7a8b9c0).
A student on another account, or the platform's own users, has it409 conflictThat email address is already registered.

In the first case, the id in the message is your existing student: fetch it with GET /v1/student/<id>, or update it. In the second case you cannot see or claim that student. Ask for a different address.

Addresses are compared without regard to letter case: Ada.Lovelace@Example.org is the same address as ada.lovelace@example.org. An address with spaces around it is refused with 400 before any comparison, so trim addresses on your side.

Warning

Registration is not idempotent. Sending the same registration twice never creates two students. The second attempt is refused with 409, which is how you know. If a registration times out, retry it: a 409 that names an id means the first attempt worked, and that id is your student. If two identical registrations run at the same moment, the loser may get the message without an id. Look the student up in your list before you treat the address as taken elsewhere. Retries and idempotency covers this pattern.

An update that moves a student to an address another student already holds is refused with 409 and That email address is already registered., without an id, even when the other student is one of yours.

Read students

All your students

curl "https://api.main-team.org/v1/student?page=1&limit=50" \
  -H "Authorization: Bearer $TOKEN"
{
  "success": true,
  "message": "Students fetched successfully.",
  "data": [
    {
      "_id": "6650f1a2b3c4d5e6f7a8b9c0",
      "username": "XXB1045",
      "firstName": "Ada",
      "lastName": "Lovelace",
      "fullName": "Ada Lovelace",
      "email": "ada.lovelace@example.org",
      "emailConfirmed": false,
      "phone": "+49 30 123 4567",
      "birth": "14/05/2010",
      "sex": "f",
      "country": {
        "_id": "630e0182c53dc79a6836e67e",
        "name": "GERMANY",
        "iso2": "DE",
        "iso3": "DEU",
        "createdAt": "2022-08-30T12:14:26.118Z",
        "updatedAt": "2022-08-30T12:14:26.118Z"
      },
      "city": {
        "_id": "6650e0a1b2c3d4e5f6a7b801",
        "name": "TIRANA",
        "country": "630e0182c53dc79a6836e67e"
      },
      "school": {
        "_id": "6650e0a1b2c3d4e5f6a7b8c2",
        "name": "TIRANA INTERNATIONAL SCHOOL",
        "country": "630e0182c53dc79a6836e67e",
        "city": "6650e0a1b2c3d4e5f6a7b801"
      },
      "grade": {
        "_id": "630e01826836e67ec53dc7a5",
        "name": "9",
        "createdAt": "2022-08-30T12:14:26.309Z",
        "updatedAt": "2022-08-30T12:14:26.309Z"
      },
      "supervisor": {
        "_id": "64c1d2e3f4a5b6c7d8e9f0a1",
        "firstName": "Deniz",
        "lastName": "Kaya",
        "fullName": "Deniz Kaya",
        "username": "XXT1003"
      },
      "activatedPlatformsThisSeason": ["common"],
      "createdAt": "2026-09-15T09:12:44.512Z",
      "updatedAt": "2026-09-15T09:12:44.512Z"
    }
  ],
  "pagination": { "page": 1, "limit": 50, "total": 312, "totalPages": 7 }
}

Paginate with page (default 1) and limit (default 20, at most 100; a larger value is lowered to 100). The list has no guaranteed order. Pagination shows complete loops, and what to expect if students are registered while you page.

To find students by email address, add email: one address, or up to 100 separated by commas. Only your students with one of them are listed, matched exactly and without regard to case, and pagination.total counts the matches.

curl "https://api.main-team.org/v1/student?email=ada.lovelace@example.org,grace.hopper@example.org" \
  -H "Authorization: Bearer $TOKEN"
  • An address none of your students has matches nothing, whoever else holds it. The filter never looks beyond your own students.
  • Encode + as %2B. In a query string + reads as a space, so ada+maths@example.org sent as it is arrives as no address at all and is refused.
  • An unreadable filter is refused, never ignored: an empty email, one given twice, an entry that is not an address, or more than 100 entries answers 400 with email must be given once, as one or more email addresses separated by commas (or email must list at most 100 addresses).
  • Addresses in a URL are more likely to be recorded along the way than a body. Use the filter where that is acceptable to you.

One student

curl "https://api.main-team.org/v1/student/6650f1a2b3c4d5e6f7a8b9c0" \
  -H "Authorization: Bearer $TOKEN"

The answer is 200 with the student in data, shaped like one element of the list above, and the message Student fetched successfully.

A student that is not yours answers 404 not_found, Student not found!, the same as an id that matches no student. An id that is not 24 hex characters is 400 (Invalid value for '_id': expected ObjectId.).

Students on one organization

GET /v1/<organizationId>/student and GET /v1/<organizationId>/student/<studentId> return the same core records, filtered to the students entered on that organization (Which organizations). The responses have the same shape as the flat routes. The single read takes the same <studentId>, the core _id, and answers 404 not_found, Student not found!, when the student is not yours or not entered on that organization.

Use these routes when your account's permissions are scoped to an organization, or when you want exactly the students an organization treats as yours. For your full list, use GET /v1/student.

The organization list also says, for each student, whether they have signed in to that organization at least once: signedIn is true or false. The organization keeps its own record of a student from that first sign-in (above), and applications and supervisor links there need it. Add signedIn=false to list only the students who still have to sign in, and send each a sign-in link; add signedIn=true for the students ready for applications. The filter narrows pagination.total too. Any other value is refused with 400 and signedIn must be true or false. The single read, GET /v1/<organizationId>/student/<studentId>, does not carry signedIn.

curl "https://api.main-team.org/v1/64b7f0c2a1d3e4f5a6b7c8d9/student?signedIn=false" \
  -H "Authorization: Bearer $TOKEN"

Fields returned

A student comes back with at most these fields, on every route that returns one. A field the student does not have, such as a phone never given, is left out rather than sent empty.

FieldTypeMeaning
_idstringThe student's id. On the student routes, the core record's id.
mainIdstringOn an organization copy, the core record's _id. The student routes return the core record, where it is usually absent.
usernamestringMinted at registration; read-only (Usernames).
firstName, lastNamestringAs you set them.
fullNamestringfirstName and lastName joined by a space; derived.
emailstringTrimmed, lower case.
emailConfirmedbooleantrue once the student has proved the address is theirs. Starts false.
phonestringAs you set it.
birthstringDD/MM/YYYY.
sexstringm, f or n.
country, city, school, gradeobject on reads, id on writesOn the read routes, the full reference record. On registration and updates, its id.
supervisorobject on reads, id on writesThe supervisor on the core record, as _id, firstName, lastName, fullName and username. Links made per organization do not appear here (Supervisors).
partnerobject on reads, id on writesA partner account the student is attached to on the platform, shaped like supervisor.
activatedPlatformsThisSeasonarray of stringsThe organizations the student is entered on.
createdAt, updatedAtISO 8601 timestamp

Nothing else a student record holds is ever returned: not the password, and not your account's details. Treat the list as open-ended all the same, and ignore fields you do not recognize (Versioning). The organization list adds one field to each student that is not part of the record, signedIn (Students on one organization).

Update a student

PUT /v1/student/<studentId> changes the fields you send and leaves every other field as it is. No field is required, but each one you send follows its registration rule.

curl -X PUT "https://api.main-team.org/v1/student/6650f1a2b3c4d5e6f7a8b9c0" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "grade": "10", "school": "Berlin Science High School" }'
{
  "success": true,
  "message": "Student updated successfully.",
  "data": {
    "_id": "6650f1a2b3c4d5e6f7a8b9c0",
    "username": "XXB1045",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "fullName": "Ada Lovelace",
    "email": "ada.lovelace@example.org",
    "emailConfirmed": false,
    "phone": "+49 30 123 4567",
    "birth": "14/05/2010",
    "sex": "f",
    "country": "630e0182c53dc79a6836e67e",
    "city": "6650e0a1b2c3d4e5f6a7b801",
    "school": "6650e0a1b2c3d4e5f6a7b8d7",
    "grade": "630e01826836e67ec53dc7a6",
    "activatedPlatformsThisSeason": ["common"],
    "createdAt": "2026-09-15T09:12:44.512Z",
    "updatedAt": "2026-09-16T07:40:02.931Z"
  }
}

The same fields as registration are accepted, except password, which has its own route. Rules to know:

  • The formats still apply to whatever you send: birth as a DD/MM/YYYY date that exists (31/02/2010 is refused), sex as m, f or n, country as an id, and grade, city and school resolving to something that exists.
  • firstName, lastName, birth and sex can be changed but not cleared. "" or null is refused with the field's rule, for example lastName should not be empty.
  • phone is cleared with "". null is refused with phone must be a string.
  • activatedPlatformsThisSeason is added to the stored list, never written over it (Adding organizations).
  • email cannot be removed. null is refused with email must be a valid email address, and an invalid address with email must be an email.
  • Changing the email withdraws confirmation. A new address has not been proved to belong to anyone, so emailConfirmed goes back to false, and the panel asks the student to confirm the new address before they can use it again. Sending the address the student already has, in any letter case, is not a change and leaves confirmation alone. That makes it safe to send a whole profile on every sync.
  • Renaming updates fullName, whether you change one name or both.
  • password is refused with property password should not exist. So are username, emailConfirmed and every other field registration does not accept.

Refusals: 404, code not_found, Student not found! when the student is not yours (or does not exist). 400 for a malformed id or a field that breaks its rule. 409 for an email another student holds (Duplicates).

The response is the student after the change. As with registration, reference fields come back as ids.

Update through an organization

PUT /v1/<organizationId>/student/<studentId> writes the same core record, with the same fields, rules and refusals as the flat update. Two things differ:

  • The permission is student/update on the organization in the path, so an account whose roles are scoped to one organization can still keep its students' details current.
  • It enters the student on that organization. Without activatedPlatformsThisSeason in the body, the organization in the path is added to the student's list, unless the list already contains "common". Updating the same student again adds nothing further. This is how you give a student access to an organization before creating a sign-in link for it.

The student does not need to be entered on the organization already, and the call does not touch the organization's copy of the student. The copy picks up the changes at the student's next sign-in there.

The body may be empty. {} enters the student on the organization, changes nothing else, and answers 200 with the student. If you send activatedPlatformsThisSeason, its values are added instead, and this organization is added only if you name it.

Flat and per-organization operations compared

Flat (/v1/student…)Per organization (/v1/<organizationId>/student…)
RecordThe core recordThe core record
Permission targetmto or *That organization's slug or *
RegisterYesNo; register on the flat route
List and readAll your studentsYour students entered on that organization
UpdateChanges the fields you sendChanges the fields you send, and enters the student on that organization
Set a passwordPUT /v1/student/<studentId>/passwordNot available
Link a supervisorNot availablePUT /v1/<organizationId>/student/<studentId>/supervisor

Errors

StatusCodeMessageCauseFix
400bad_requestproperty <name> should not existA field the route does not accept.Remove it.
400bad_request<field> should not be empty, email must be an email, email must be a valid email address, birth must be a real date in DD/MM/YYYY format, country must be a mongodb id, grade must be a string, …A field is missing or malformed.Fix the field named.
400bad_requestphone must be a stringphone was sent as null.Leave it out, or on an update send "" to clear it.
400bad_requestactivatedPlatformsThisSeason must be an array, each value in activatedPlatformsThisSeason must be one of the following values: …The list is null, or holds a value other than common and the five slugs.Leave it out, or send slugs from the list above.
400bad_request<field> is not a known <field>. (and This API does not create reference data. for a name)A reference value matches nothing, or names a country that cannot be selected.Take ids from the reference routes; check spelling and the student's country.
400bad_requestInvalid value for '_id': expected ObjectId.<studentId> is not 24 hex characters.Use the _id from registration.
400bad_requestemail must be given once, as one or more email addresses separated by commas, email must list at most 100 addressesThe email filter of GET /v1/student could not be read.Send one comma-separated list of up to 100 addresses, with + encoded as %2B.
400bad_requestsignedIn must be true or falseThe signedIn filter of GET /v1/<organizationId>/student is another value.Send true or false, or leave it out.
400bad_requestproperty password should not existpassword was sent to POST /v1/student/check.Leave it out; send it with the registration.
403forbiddenInsufficient role permissionsThe account lacks the permission, or holds it on another organization.Ask the operator for the role.
403forbiddenSetting a student's password needs the auth/signin permission on mto, …password in a registration without auth/signin on mto.See Passwords.
404not_foundStudent not found!A read or update of a student that is not yours or does not exist, or a read through an organization the student has no access to.Check the id and the account.
404not_foundOrganization not found!<organizationId> is not an organization _id.Use the _id from GET /v1/organization, never the slug.
409conflictA student with that email is already registered to this account (<id>).The address is one of your students'.Use that student.
409conflictThat email address is already registered.The address belongs to a student elsewhere.Ask for another address.

Search the API documentation

Guides, endpoints by name, path or permission, and error codes such as not_found.