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.
A link belongs to one organization
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.
Before you link
Three things must be true, and the API does not tell you which one failed (see Every refusal is the same 404):
- The student is yours.
<studentId>is the_idyou got when you registered the student, and the student belongs to your account. - 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.
- 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.
Link a student
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" }'
| Field | Type | Required | Rules |
|---|---|---|---|
supervisorUsername | string | yes | The 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:
| Field | Meaning |
|---|---|
organization | The slug of the organization the link was made on. |
student | The 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._id | That organization's own id for the student. It differs from the id you registered the student with. |
student.mainId | The id you registered the student with. Match on this. |
student.supervisor | The supervisor's id on this organization. |
supervisor | The 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:
| Cause | How to check | Fix |
|---|---|---|
| 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:
| Status | Code | Message | Cause |
|---|---|---|---|
400 | bad_request | supervisorUsername should not be empty or supervisorUsername must be a string | The field is missing, empty, or not a string. |
400 | bad_request | property <name> should not exist | The body has a field other than supervisorUsername. |
400 | bad_request | Invalid value for '_id': expected ObjectId. | <studentId> is not 24 hex characters. |
401 | unauthorized | Authentication is required or the provided credentials are invalid. | The token was refused. |
403 | forbidden | Insufficient role permissions | No student/update on this organization. |
404 | not_found | Organization not found! | <organizationId> is not an organization _id. A slug such as stem is not accepted. |
Change, repeat or remove a link
- 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
_idand 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']})");
}
Related
- 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
_idon every organization.