# Organizations

> The core record (mto) and the five olympiads, which routes act on which, why paths take the organization _id, and what "your students" means.

The platform behind this API is one core record plus five olympiads. Knowing which data lives where explains most of the API's shape: why some paths have an organization id and others do not, why a student has to sign in before you can enter them for an exam, and why an id sometimes seems to "not match".

## One core record, five olympiads

| Slug | What it is | What lives there |
|---|---|---|
| `mto` | **The core record** of the Main Team platform | Partner accounts, every student's core record (name, email, grade, country, password), and the shared reference data: countries, grades and the list of organizations |
| `stem` | STEM olympiad | Its own exams, exam categories, applications, certificates and reports, and its own copy of each student who has signed in there |
| `hilingua` | Language olympiad | The same, for this olympiad |
| `neo` | Science olympiad | The same, for this olympiad |
| `gmath` | Mathematics olympiad | The same, for this olympiad |
| `coding` | Coding olympiad | The same, for this olympiad |

The `name` field of `GET /v1/organization` has each organization's official name. Use the **slug** in your code; it is short and stable.

A student exists once on the core record and, in addition, once in each olympiad they have signed in to (see [Organization copies](#organization-copies-of-a-student)). Exams, applications and results always belong to one olympiad.

## Two kinds of routes

The path tells you which part of the platform a route acts on.

| Kind | Path | Acts on | Examples |
|---|---|---|---|
| **Flat** | `/v1/<resource>` (no organization id) | The core record, `mto` | `/v1/student`, `/v1/country`, `/v1/grade`, `/v1/organization`, `/v1/api-account/...` |
| **Per organization** | `/v1/{organizationId}/<resource>` | The organization whose `_id` is in the path | `/v1/{organizationId}/exam`, `/v1/{organizationId}/application`, `/v1/{organizationId}/auth/signin` |

The complete list:

| Flat routes (act on `mto`) | Per-organization routes |
|---|---|
| `GET /v1/api-account/validate-me`, `POST /v1/api-account/revoke-token` | `GET`, `PUT /v1/{organizationId}/student...` and `PUT .../student/{studentId}/supervisor` |
| `GET /v1/country`, `GET /v1/country/{id}` | `POST /v1/{organizationId}/auth/signin` |
| `GET /v1/grade`, `GET /v1/grade/{id}` | `GET /v1/{organizationId}/exam-category...` |
| `GET /v1/organization`, `GET /v1/organization/{id}` | `GET /v1/{organizationId}/exam...` |
| `GET`, `POST /v1/student`, `GET`, `PUT /v1/student/{studentId}`, `PUT /v1/student/{studentId}/password` | `GET`, `POST`, `PUT`, `DELETE /v1/{organizationId}/application...` |
| | `GET /v1/{organizationId}/certificate...`, `GET /v1/{organizationId}/report...` |

This split also decides your permissions. A flat route needs a role whose target is `mto` or `*`. A per-organization route needs a role whose target is that organization's slug or `*`. See [Permissions](https://hub.main-team.org/api/permissions#target).

## The organization id

Every per-organization path starts with `{organizationId}`: **the `_id` of the organization, as returned by `GET /v1/organization`**. It is 24 hexadecimal characters, such as `64b7f0c2a1e4d5f6a7b8c9d1`.

The slug does not work in the path. `/v1/stem/exam` answers `404 not_found` with `Organization not found!`, just as an id that belongs to no listed organization does. Always put the `_id` there.

### List organizations

```bash
curl -s "https://api.main-team.org/v1/organization?limit=100" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "success": true,
  "message": "Organizations fetched successfully.",
  "data": [
    {
      "_id": "64b7f0c2a1e4d5f6a7b8c9d1",
      "name": "STEM Olympiad",
      "slug": "stem",
      "logo": "https://example.org/logos/stem.png",
      "desc": "International STEM olympiad.",
      "defaultRedirect": "https://my.example-stem.org"
    },
    {
      "_id": "64b7f0c2a1e4d5f6a7b8c9d2",
      "name": "Hi-Lingua",
      "slug": "hilingua",
      "logo": "https://example.org/logos/hilingua.png",
      "desc": "International language olympiad.",
      "defaultRedirect": "https://my.example-hilingua.org"
    }
  ],
  "pagination": { "page": 1, "limit": 100, "total": 5, "totalPages": 1 }
}
```

(Example data, shortened to two of the five organizations.)

The list holds the five olympiads. `mto`, the core record, is not in it: every flat route acts on `mto`, so you never need its id.

| Field | Meaning |
|---|---|
| `_id` | The id to put in `/v1/{organizationId}/...` |
| `slug` | The short name, such as `stem`. Role targets use it too, and a role can name only `mto`, `stem`, `hilingua`, `neo`, `gmath`, `coding` or `*` |
| `name` | Display name |
| `logo` | Logo image URL, when the organization has one |
| `desc` | Short description, when the organization has one |
| `defaultRedirect` | Where that organization's student panel lives. A sign-in link lands there when you do not name a page |

Fields an organization does not have are left out of its entry, so treat every field except `_id` and `slug` as optional.

The list is paginated like every list (`page` defaults to 1, `limit` to 20, and values above 100 are lowered to 100). With `limit=100` you get every organization in one request. See [Pagination](https://hub.main-team.org/api/pagination).

Needs `organization/read` on `mto`. Reference: [listOrganizations](https://hub.main-team.org/api/reference/list-organizations).

### Get one organization

```bash
curl -s "https://api.main-team.org/v1/organization/64b7f0c2a1e4d5f6a7b8c9d1" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "success": true,
  "message": "Organization fetched successfully.",
  "data": {
    "_id": "64b7f0c2a1e4d5f6a7b8c9d1",
    "name": "STEM Olympiad",
    "slug": "stem",
    "logo": "https://example.org/logos/stem.png",
    "desc": "International STEM olympiad.",
    "defaultRedirect": "https://my.example-stem.org"
  }
}
```

An id that names none of the listed organizations, `mto`'s included, answers `404 not_found` with `Organization not found!`. A malformed id, such as `abc`, answers `400 bad_request` with `Invalid value for '_id': expected ObjectId.`. (This route is flat, so it does not go through the organization check described below, where a malformed id answers `404` too.)

Needs `organization/read` on `mto`. Reference: [getOrganization](https://hub.main-team.org/api/reference/get-organization).

### Keep a slug-to-id map

Organizations rarely change. Fetch the list when your integration starts, keep a map from slug to `_id`, and refresh it once a day, or whenever a call answers `404` with `Organization not found!`.

```js
let orgIds = null; // { stem: '...', hilingua: '...', ... }

async function organizationId(slug) {
  if (!orgIds) {
    const { data } = await api('/organization?limit=100');
    orgIds = Object.fromEntries(data.map((o) => [o.slug, o._id]));
  }
  const id = orgIds[slug];
  if (!id) throw new Error(`Unknown organization slug: ${slug}`);
  return id;
}

// GET /v1/{stem's _id}/exam
const exams = await api(`/${await organizationId('stem')}/exam`);
```

(`api()` is the helper from [Authentication](https://hub.main-team.org/api/authentication#nodejs).)

The list is not filtered by your roles, so it may include organizations you have no role on. That is harmless: a call to one of them answers `403 forbidden`. Work only with the organizations you agreed with the operator, and look them up by slug rather than by their position in the list, because the list has no guaranteed order.

### How the id is checked

On a per-organization route the API checks, in this order:

| Order | Check | If it fails |
|---|---|---|
| 1 | Your token | `401 unauthorized`, whatever the organization id. Someone without a valid token cannot find out which organizations exist |
| 2 | The organization id is 24 hexadecimal characters and names an organization that `GET /v1/organization` lists | `404 not_found`, `Organization not found!` |
| 3 | Your roles allow this action on this organization | `403 forbidden`, `Insufficient role permissions` |
| 4 | Your rate limit for this route | `429 too_many_requests` |
| 5 | The request body and the route's own rules | `400`, `404`, `409` and so on, as each route documents |

So an unknown organization id answers `404` even when your roles would not have allowed the call, and the id's letter case does not matter. Requests refused at steps 1 to 3 do not count against your rate limit.

One thing happens before all of these steps: reading the request body. A body over 100 kB (`413 payload_too_large`), a body in an encoding the API does not read (`415 unsupported_media_type`), or a body that is not valid JSON (`400 bad_request`) is refused before the token is even looked at. See [Requests and responses](https://hub.main-team.org/api/requests-and-responses).

## Your students

A student belongs to the partner account that registered them, through `POST /v1/student`. Every route that reads or changes a student, or anything reached through a student (applications, certificates, reports, sign-in links, supervisor links), only ever finds **your own** students.

### A foreign record looks like a missing one

The API never tells you that a record exists but belongs to someone else. A student, application, certificate or report that belongs to another partner answers exactly as if it did not exist:

- Every route answers `404 not_found`, with the message it gives for a missing record: `Student not found!` for a student, `Application not found!` for an application.
- Some routes use one message for every refusal of their own. The supervisor link answers `Not found!` whatever check failed, and the certificate and report downloads answer `Not found!` for a document that is missing, not yet released, not yours, or has no file.

This is deliberate. If a partner could tell "exists but not yours" from "does not exist", it could probe ids to learn about other partners' students.

### A new account does not see old students

Ownership is tied to your **account**, not your company. If an operator issues you a replacement account (after a leaked secret, for example), the new account does not see the students the old one registered. Talk to the operator about those students before you switch.

### Which students an organization route shows

The student routes under an organization, `GET` and `PUT /v1/{organizationId}/student...`, work on the same core records as the flat `/v1/student` routes, filtered to your students **who have access to that organization**:

Access is held in the student's `activatedPlatformsThisSeason` list. `common` in that list means "every organization"; otherwise the list names the slugs the student may use.

| You do | The student's access afterwards |
|---|---|
| `POST /v1/student` without `activatedPlatformsThisSeason` | `["common"]`: every organization. This is the default |
| `POST /v1/student` with a list, for example `["stem"]` | Only the organizations on the list |
| `PUT /v1/{organizationId}/student/{studentId}` without `activatedPlatformsThisSeason`, even with an empty body `{}` | That organization is added, unless the student already has `common` |
| `PUT /v1/{organizationId}/student/{studentId}` with a list | The organizations on your list are added. The organization in the path is added only if it is on your list |
| `PUT /v1/student/{studentId}` with a list | The organizations on your list are added |

An update only ever adds. Nothing you send removes an organization from a student's list: a value the list already holds is not added twice, and `[]` adds nothing. To give a student one more organization, send `{}` to that organization's update route, or send the slug alone. You never need to read the current list first.

A sign-in link for a student without access to the organization answers `403 forbidden` with `Student is not activated for organization <slug>.`. The student guide covers the field in full: see [Students](https://hub.main-team.org/api/guides/students).

## Organization copies of a student

Each olympiad keeps **its own copy** of every student who has signed in there. That copy has its own `_id` in the olympiad, and it points back to the core record through the field `mainId`. Applications, certificates and reports belong to the olympiad's copy.

**The copy is created the first time the student signs in to that olympiad.** For an integration, that means the student following a sign-in link you minted with `POST /v1/{organizationId}/auth/signin`. No API call creates the copy directly; only a sign-in does.

Until the student has signed in to an olympiad once (the examples use stem; the message names the olympiad you called):

| You call | You get |
|---|---|
| `POST /v1/{organizationId}/application` | `409 conflict`: `Student has never signed in to stem, so stem holds no record for them. Generate a sign-in link first with POST /:organizationId/auth/signin.` |
| `GET /v1/{organizationId}/application/student-applications/{studentId}` | The same `409 conflict` |
| `GET /v1/{organizationId}/certificate/{userId}` and `GET /v1/{organizationId}/report/{userId}` | The same `409 conflict` |
| `PUT /v1/{organizationId}/student/{studentId}/supervisor` | `404 not_found`, `Not found!` (the supervisor link uses one answer for every refusal) |
| `GET /v1/{organizationId}/application` and `GET /v1/{organizationId}/application/exam-applications/{examId}` | `200`. The student is simply not in the results |
| `GET /v1/{organizationId}/exam/available/{studentId}` | `200`, as long as the student has a grade. The exam picker works before the first sign-in, so you can show the choices early |

The `409` comes after the ownership check, so a student who is not yours still answers `404` with `Student not found!`.

So the usual order for a new student on an olympiad is:

1. Register the student on the core record: `POST /v1/student`.
2. Mint a sign-in link on the olympiad and send the student to it: `POST /v1/{organizationId}/auth/signin`. Their first sign-in creates the olympiad's copy.
3. Now enter them for exams and link supervisors on that olympiad.

The [register and apply tutorial](https://hub.main-team.org/api/tutorials/register-and-apply) walks through this end to end, and [Sign-in links](https://hub.main-team.org/api/guides/sign-in-links) covers the link itself.

### Which id to use

Always identify a student by the **core id**: the `_id` that `POST /v1/student` returned. Every route that takes a `studentId` expects it, including the per-organization routes.

In responses that come from an olympiad (applications, for example), a student appears with both `_id` and `mainId`. There, `_id` is the olympiad's copy and `mainId` is the core id you know. **Match on `mainId`.** See [Identifiers](https://hub.main-team.org/api/identifiers).

## Where to go next

- The permission model and the target of a role: [Permissions](https://hub.main-team.org/api/permissions).
- Registering and updating students: [Students](https://hub.main-team.org/api/guides/students).
- Countries and grades for registration: [Reference data](https://hub.main-team.org/api/guides/reference-data).
- Exams and what "open" means: [Exams](https://hub.main-team.org/api/guides/exams).
