Skip to content
API documentation
View as MarkdownOpen in Claude

Guides

Supervisors

A supervisor is a teacher's or coach's account on an organization. You can link one of your students to a supervisor there by the supervisor's username:

PUT /v1/<organizationId>/student/<studentId>/supervisor

That is the whole API surface for supervisors: one operation, Link a student to a supervisor. The API never creates, lists, changes or removes supervisor accounts. Supervisors register on the platform themselves. When you want to link a student, ask the supervisor for their username.

Each organization keeps its own record of a student and of each supervisor, so a supervisor link is made on one organization and stays there. Linking a student to XXT1003 on stem changes nothing on hilingua, neo or any other organization. To link the same student on two organizations, call the route once for each, with each organization's _id in the path.

The same person can have a supervisor account on several organizations under one username. The link always uses the supervisor account on the organization in the path.

Links are made on the olympiads: stem, hilingua, neo, gmath and coding, the organizations GET /v1/organization lists. mto holds the core record itself, not an organization copy, so it has nothing to link.

Three things must be true, and the API does not tell you which one failed (see Every refusal is the same 404):

  1. The student is yours. <studentId> is the _id you got when you registered the student, and the student belongs to your account.
  2. The student has signed in to this organization at least once. The link is written to that organization's copy of the student, and the copy only exists after the student's first sign-in there, through a sign-in link you create or on their own. The API cannot create the copy for you. Until it exists there is nothing to attach a supervisor to.
  3. The username belongs to a supervisor on this organization. Not a student, and not a supervisor who only has an account on another organization.

Note

Right after registration, link on the next step, not the same one. A newly registered student has no copy on any organization yet. The usual order is: register → create a sign-in link for this organization → the student opens it → link the supervisor. The link-in step creates the copy.

The account needs student/update on the organization in the path: a role with target that organization's slug or *.

curl -X PUT "https://api.main-team.org/v1/64b7f0c2a1e4d93b5c2f1a02/student/6650f1a2b3c4d5e6f7a8b9c0/supervisor" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "supervisorUsername": "XXT1003" }'
FieldTypeRequiredRules
supervisorUsernamestringyesThe supervisor's username. Letter case does not matter, and spaces around it are ignored: " xxt1003 " finds XXT1003.

The body takes that one field. Any other property is refused with 400, property <name> should not exist.

{
  "success": true,
  "message": "Student linked to supervisor successfully.",
  "data": {
    "organization": "stem",
    "student": {
      "_id": "6651a7b8c9d0e1f2a3b4c5d6",
      "mainId": "6650f1a2b3c4d5e6f7a8b9c0",
      "username": "XXB1045",
      "firstName": "Katherine",
      "lastName": "Lovelace",
      "fullName": "Katherine Lovelace",
      "email": "k.pioneer@example.org",
      "emailConfirmed": false,
      "supervisor": "64c1d2e3f4a5b6c7d8e9f0a1",
      "createdAt": "2026-09-02T08:30:11.204Z",
      "updatedAt": "2026-09-15T10:21:47.655Z"
    },
    "supervisor": {
      "_id": "64c1d2e3f4a5b6c7d8e9f0a1",
      "username": "XXT1003"
    }
  }
}

The example shows only some of student's fields. What each part means:

FieldMeaning
organizationThe slug of the organization the link was made on.
studentThe organization's copy of the student, after the change. It has the same kinds of fields as a student anywhere in the API (Students), as that organization holds them.
student._idThat organization's own id for the student. It differs from the id you registered the student with.
student.mainIdThe id you registered the student with. Match on this.
student.supervisorThe supervisor's id on this organization.
supervisorThe supervisor who was linked: their id on this organization and their username.

Warning

Keep the response; the student reads will not show the link. GET /v1/student/<studentId> and GET /v1/<organizationId>/student/<studentId> return the core record, and a link made on an organization lives on that organization's copy. The supervisor you see on those reads is not this link. If you need to know later who supervises a student on which organization, record it from this response.

Every refusal is the same 404

If the link cannot be made, the answer is always the same:

{
  "error": {
    "code": "not_found",
    "message": "Not found!",
    "documentation_url": "https://hub.main-team.org/api/errors#not_found",
    "request_id": "c4e2a9b7-1d3f-4a6e-8b5c-2f0e9d7a6b31"
  }
}

The status and message are identical whatever the reason, and the response never repeats the username you sent. That is deliberate. Otherwise the route could be used to find out which usernames exist, which of them are supervisors, and which students belong to which account. The cost is that you have to work out the cause yourself. Go through this list in order:

CauseHow to checkFix
The student is not on your account, or the id is wrong.GET /v1/student/<studentId> answers 404, Student not found!.Use the _id from registration, with a token for the account that registered the student.
The student has never signed in to this organization.The API cannot show this directly. It is the likely cause if the student was registered recently, or has only ever used another organization.Create a sign-in link for this organization (Sign-in links), have the student open it, then link again.
The username has a typo, or nobody on this organization has it.Ask the supervisor to read their username back to you.Correct it and link again.
The username belongs to someone who is not a supervisor on this organization, such as a student, or a supervisor on another organization only.Ask the supervisor which organization they registered on.Use the right organization's _id, or ask the supervisor to register on this one.
The username is blank or only spaces.Look at the body you sent.Send the username.

Nothing is written for any refusal.

Other refusals

These come from checks that run before the link is attempted, so they do say what went wrong:

StatusCodeMessageCause
400bad_requestsupervisorUsername should not be empty or supervisorUsername must be a stringThe field is missing, empty, or not a string.
400bad_requestproperty <name> should not existThe body has a field other than supervisorUsername.
400bad_requestInvalid value for '_id': expected ObjectId.<studentId> is not 24 hex characters.
401unauthorizedAuthentication is required or the provided credentials are invalid.The token was refused.
403forbiddenInsufficient role permissionsNo student/update on this organization.
404not_foundOrganization not found!<organizationId> is not an organization _id. A slug such as stem is not accepted.
  • Link again with another username to change the supervisor. A student has one supervisor per organization, and the new link replaces the old one.
  • Link again with the same username and nothing changes. You get the same successful response. Retrying after a timeout is safe.
  • Removing a link is not possible through the API. Write to info@main-team.org with the student's _id and the organization.

A link stays in place when the student signs in again later. Signing in brings the organization's copy up to date with the student's profile, but it leaves the supervisor alone.

Anyone who is a supervisor there can be linked

The route accepts any supervisor on the organization, not only supervisors connected to your account. A mistyped username that happens to belong to another real supervisor links your student to that person. Take usernames from the supervisors themselves, check them before you send them, and show the result (supervisor.username in the response) to whoever asked for the link.

A complete example

Register-then-link is two separate moments in time: the student has to sign in to the organization in between. This helper links a student and turns the one 404 into a useful error for your own staff.

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

export async function linkSupervisor(token, organizationId, studentId, supervisorUsername) {
  const res = await fetch(`${BASE}/${organizationId}/student/${studentId}/supervisor`, {
    method: 'PUT',
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ supervisorUsername }),
  });
  const body = await res.json();

  if (res.ok) {
    const { organization, student, supervisor } = body.data;
    // Store the link on your side: the student reads will not show it.
    return { organization, studentId: student.mainId, supervisor: supervisor.username };
  }

  const { code, message, request_id } = body.error;
  if (res.status === 404 && message === 'Not found!') {
    throw new Error(
      `Supervisor link refused (request ${request_id}). Check: the student is ours; ` +
        `the student has signed in to this organization at least once; ` +
        `"${supervisorUsername}" is a supervisor on this organization.`,
    );
  }
  throw new Error(`${res.status} ${code}: ${message} (request ${request_id})`);
}
<?php
const MT_BASE = 'https://api.main-team.org/v1';

function mt_link_supervisor(string $token, string $organizationId, string $studentId, string $username): array
{
    $url = MT_BASE . '/' . rawurlencode($organizationId) . '/student/' . rawurlencode($studentId) . '/supervisor';
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => 'PUT',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode(['supervisorUsername' => $username]),
        CURLOPT_TIMEOUT => 30,
    ]);
    $raw = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $body = json_decode($raw, true);

    if ($status === 200) {
        $data = $body['data'];
        // Store the link on your side: the student reads will not show it.
        return [
            'organization' => $data['organization'],
            'studentId' => $data['student']['mainId'],
            'supervisor' => $data['supervisor']['username'],
        ];
    }

    $error = $body['error'];
    if ($status === 404 && $error['message'] === 'Not found!') {
        throw new RuntimeException(
            "Supervisor link refused (request {$error['request_id']}). Check: the student is ours; "
            . "the student has signed in to this organization at least once; "
            . "\"$username\" is a supervisor on this organization."
        );
    }
    throw new RuntimeException("$status {$error['code']}: {$error['message']} (request {$error['request_id']})");
}
  • Sign-in links: how the student's first sign-in creates the organization's copy.
  • Students: the core record, organization copies and mainId.
  • Identifiers: why the same person has a different _id on every organization.

Search the API documentation

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