# Reference data

> Look up countries, grades and organizations, the ids that student registration and every organization route depend on.

Three read-only directories supply the ids the rest of the API asks for:

| Directory | What you use it for | Operations |
|---|---|---|
| Countries | The `country` field when you [register a student](https://hub.main-team.org/api/guides/students#register-a-student) | [List countries](https://hub.main-team.org/api/reference/list-countries), [Get a country](https://hub.main-team.org/api/reference/get-country) |
| Grades | The `grade` field at registration, and the grade an exam is restricted to | [List grades](https://hub.main-team.org/api/reference/list-grades), [Get a grade](https://hub.main-team.org/api/reference/get-grade) |
| Organizations | The `<organizationId>` in every `/v1/<organizationId>/…` path | [List organizations](https://hub.main-team.org/api/reference/list-organizations), [Get an organization](https://hub.main-team.org/api/reference/get-organization) |

The API never creates, changes or deletes reference data. Each of these routes is a plain read, and
nothing you send to any other route adds a country, grade, city or school. A value that matches
nothing is refused (see [How reference fields resolve](https://hub.main-team.org/api/guides/students#how-reference-fields-resolve)).

## Permissions

All six routes are flat routes: they have no `<organizationId>` in the path, so they act on the core
record (`mto`). A role that grants one needs target `mto` or `*`.

| Route | Permission |
|---|---|
| `GET /v1/country`, `GET /v1/country/<id>` | `country/read` |
| `GET /v1/grade`, `GET /v1/grade/<id>` | `grade/read` |
| `GET /v1/organization`, `GET /v1/organization/<id>` | `organization/read` |

Without the permission the answer is `403` with code [`forbidden`](https://hub.main-team.org/api/errors#forbidden) and the message
`Insufficient role permissions`. The [Permissions](https://hub.main-team.org/api/permissions) page explains how roles match.

## Countries

### List countries

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

```json
{
  "success": true,
  "message": "Countries fetched successfully.",
  "data": [
    {
      "_id": "630e0182c53dc79a6836e67e",
      "name": "GERMANY",
      "iso2": "DE",
      "iso3": "DEU",
      "createdAt": "2022-08-30T12:14:26.118Z",
      "updatedAt": "2022-08-30T12:14:26.118Z"
    },
    {
      "_id": "630e0182c53dc79a6836e67f",
      "name": "ALGERIA",
      "iso2": "DZ",
      "iso3": "DZA",
      "createdAt": "2022-08-30T12:14:26.118Z",
      "updatedAt": "2022-08-30T12:14:26.118Z"
    }
  ],
  "pagination": { "page": 1, "limit": 100, "total": 246, "totalPages": 3 }
}
```

The list is paginated like every listing: `page` defaults to 1, and `limit` defaults to 20 with a
maximum of 100. A larger `limit` is lowered to 100 rather than refused (see [Pagination](https://hub.main-team.org/api/pagination)).
The list has no guaranteed order, so page through all of it and index it yourself.

| Field | Type | Notes |
|---|---|---|
| `_id` | string, 24 hex characters | The value `country` takes at registration. |
| `name` | string | Stored in upper case, for example `"GERMANY"`. |
| `iso2` | string | Two-letter code, upper case. It is also the first two characters of every username minted for a student in that country. |
| `iso3` | string | Three-letter code, upper case. |
| `tz`, `dialCode`, `flag` | string | Present on some countries. |
| `createdAt`, `updatedAt` | ISO 8601 timestamp | |

A country document may carry other fields, such as `__v`. Ignore fields you do not use.

**Some countries cannot be selected.** They are left out of the list and out of the total, and
`GET /v1/country/<id>` answers for one of them exactly as it answers for an id that does not exist.
A registration or update that names one is refused with `400` and `country is not a known country.`

### Get one country

```bash
curl "https://api.main-team.org/v1/country/630e0182c53dc79a6836e67e" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "success": true,
  "message": "Country fetched successfully.",
  "data": {
    "_id": "630e0182c53dc79a6836e67e",
    "name": "GERMANY",
    "iso2": "DE",
    "iso3": "DEU",
    "createdAt": "2022-08-30T12:14:26.118Z",
    "updatedAt": "2022-08-30T12:14:26.118Z"
  }
}
```

### Why `country` takes an id and nothing else

At registration `grade`, `city` and `school` accept a name, but `country` accepts only the `_id` from
this list. A country has three common spellings (its name, its ISO2 code and its ISO3 code), and
choosing between them is ambiguity the field does not need. The country also scopes how a city or
school name is looked up, so it has to be exact. Build a lookup from `iso2` (or whatever code your
own records use) to `_id` once, and translate on your side.

## Grades

### List grades

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

```json
{
  "success": true,
  "message": "Grades fetched successfully.",
  "data": [
    {
      "_id": "630e01826836e67ec53dc79d",
      "name": "1",
      "createdAt": "2022-08-30T12:14:26.301Z",
      "updatedAt": "2022-08-30T12:14:26.301Z"
    },
    {
      "_id": "630e01826836e67ec53dc79e",
      "name": "2",
      "createdAt": "2022-08-30T12:14:26.302Z",
      "updatedAt": "2022-08-30T12:14:26.302Z"
    }
  ],
  "pagination": { "page": 1, "limit": 100, "total": 12, "totalPages": 1 }
}
```

Grades come back in the order they were created, oldest first. They are not sorted by `name`: names
are strings, and sorting them as text would put `"10"` before `"2"`.

| Field | Type | Notes |
|---|---|---|
| `_id` | string, 24 hex characters | The same id on every organization. |
| `name` | string | The grade as a school would say it, for example `"10"`. |
| `createdAt`, `updatedAt` | ISO 8601 timestamp | |

### Grade ids are the same everywhere

A grade has the same `_id` on the core record and on every organization. That is why a student's grade
can be checked directly against the grades an exam accepts, on any organization, without translation.
It is also why the API is strict about the `grade` field: a grade that does not exist in this list is a
student no exam will ever accept.

### A name works too

When you register or update a student, `grade` takes either an `_id` from this list or its `name`.
`"grade": "10"` is resolved to the grade whose name is `10`, and that grade's `_id` is what gets stored,
so the two forms end in the same place. A name that matches no grade is refused with `400` and
`grade is not a known grade. This API does not create reference data.` This list shows which names are
accepted.

### Get one grade

```bash
curl "https://api.main-team.org/v1/grade/630e01826836e67ec53dc79d" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "success": true,
  "message": "Grade fetched successfully.",
  "data": {
    "_id": "630e01826836e67ec53dc79d",
    "name": "1",
    "createdAt": "2022-08-30T12:14:26.301Z",
    "updatedAt": "2022-08-30T12:14:26.301Z"
  }
}
```

## Organizations

### List organizations

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

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

The list holds the five organizations that run exams: stem, hilingua, neo, gmath and coding. `mto`,
the core record, is not listed. The routes without an `<organizationId>` act on it, so you never
need its id, and [`GET /v1/organization/<id>`](https://hub.main-team.org/api/reference/get-organization) answers `404` for it.

An organization comes back with exactly these fields:

| Field | Type | Notes |
|---|---|---|
| `_id` | string, 24 hex characters | The `<organizationId>` every `/v1/<organizationId>/…` route takes. |
| `name` | string | Display name. |
| `slug` | string | Short name: `stem`, `hilingua`, `neo`, `gmath` or `coding`. Used in role targets and in a student's `activatedPlatformsThisSeason`, **never in a path**. |
| `logo` | string | Logo URL, when set. |
| `desc` | string | Description, when set. |
| `defaultRedirect` | string | Where a [sign-in link](https://hub.main-team.org/api/guides/sign-in-links) sends the student when you give no `redirect`, when set. |

### Use the `_id`, never the slug

`/v1/stem/exam` does not work. The path segment must be the organization's `_id`:
`/v1/64b7f0c2a1e4d93b5c2f1a02/exam`. A segment that is not 24 hex characters, or that names no
organization, is answered with `404`, code [`not_found`](https://hub.main-team.org/api/errors#not_found) and the message
`Organization not found!`. That check runs after your token is checked, so a request without a valid
token gets `401` whatever the path says.

```json
{
  "error": {
    "code": "not_found",
    "message": "Organization not found!",
    "documentation_url": "https://hub.main-team.org/api/errors#not_found",
    "request_id": "3f9d2c1e-7a4b-4e0f-9c3d-1b2a5e6f7d80"
  }
}
```

Organization ids do not change, so fetch the list once, keep a map from `slug` to `_id`, and use the
map everywhere you build a path. [Organizations](https://hub.main-team.org/api/organizations) explains what the core record
and the five olympiads are, and why the flat routes act on `mto`.

### Get one organization

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

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

## Ids that are unknown or malformed

| You send | `GET /v1/country/<id>` | `GET /v1/grade/<id>` | `GET /v1/organization/<id>` |
|---|---|---|---|
| A well-formed id that matches nothing | `404`, `Country not found!` | `404`, `Grade not found!` | `404`, `Organization not found!` |
| Anything that is not 24 hex characters | `400`, `Invalid value for '_id': expected ObjectId.` | the same | the same |

A country that cannot be selected answers like an unknown id, and so does an organization the list
leaves out, `mto` included. Every `404` here has the code [`not_found`](https://hub.main-team.org/api/errors#not_found), and
every `400` the code [`bad_request`](https://hub.main-team.org/api/errors#bad_request).

## Cities and schools

There is no route that lists cities or schools. At registration you name them instead: `city` is
looked up by name within the student's country, and `school` within that country and city. A name the
platform does not know is refused, and the API cannot add one. If a real city or school is refused,
write to [info@main-team.org](mailto:info@main-team.org) with the country, city and school as your
records have them. [Students](https://hub.main-team.org/api/guides/students#how-reference-fields-resolve) describes the lookup
rules in full.

## Caching

Reference data changes rarely, and each read counts against your [rate limit](https://hub.main-team.org/api/rate-limits)
(100 requests per 60 seconds per operation). A sensible pattern:

- Load all organizations, countries and grades when your integration starts, and keep them in memory
  or in your own store.
- Refresh them once a day.
- Refresh early when a student write is refused with `country is not a known country.` or
  `grade is not a known grade.` for an id you took from your cache. Then retry once with the fresh
  data.

This loader builds the three maps you need. It follows `totalPages`, so it works however large a
list grows.

```js [Node.js]
const BASE = 'https://api.main-team.org/v1';

async function getAll(path, token) {
  const items = [];
  for (let page = 1; ; page++) {
    const res = await fetch(`${BASE}${path}?page=${page}&limit=100`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`);
    const body = await res.json();
    items.push(...body.data);
    if (page >= body.pagination.totalPages) return items;
  }
}

export async function loadReferenceData(token) {
  const [organizations, countries, grades] = await Promise.all([
    getAll('/organization', token),
    getAll('/country', token),
    getAll('/grade', token),
  ]);
  return {
    organizationIdBySlug: new Map(organizations.map((o) => [o.slug, o._id])),
    countryIdByIso2: new Map(countries.map((c) => [c.iso2, c._id])),
    gradeIdByName: new Map(grades.map((g) => [g.name, g._id])),
  };
}

// const ref = await loadReferenceData(token);
// ref.organizationIdBySlug.get('stem')  // "64b7f0c2a1e4d93b5c2f1a02"
// ref.countryIdByIso2.get('AL')         // "630e0182c53dc79a6836e67e"
```

```php [PHP]
<?php
const MT_BASE = 'https://api.main-team.org/v1';

function mt_get_all(string $path, string $token): array
{
    $items = [];
    for ($page = 1; ; $page++) {
        $ch = curl_init(MT_BASE . $path . '?page=' . $page . '&limit=100');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
            CURLOPT_TIMEOUT => 30,
        ]);
        $raw = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);
        if ($raw === false || $status !== 200) {
            throw new RuntimeException("$path: HTTP $status");
        }
        $body = json_decode($raw, true);
        array_push($items, ...$body['data']);
        if ($page >= $body['pagination']['totalPages']) {
            return $items;
        }
    }
}

function mt_load_reference_data(string $token): array
{
    $organizations = mt_get_all('/organization', $token);
    $countries = mt_get_all('/country', $token);
    $grades = mt_get_all('/grade', $token);

    return [
        'organizationIdBySlug' => array_column($organizations, '_id', 'slug'),
        'countryIdByIso2' => array_column($countries, '_id', 'iso2'),
        'gradeIdByName' => array_column($grades, '_id', 'name'),
    ];
}

// $ref = mt_load_reference_data($token);
// $ref['organizationIdBySlug']['stem']; // "64b7f0c2a1e4d93b5c2f1a02"
```

When a list is empty, `totalPages` is `0`. Both loops stop after the first page either way.

## Related

- [Students](https://hub.main-team.org/api/guides/students): how `country`, `grade`, `city` and `school` are resolved when you
  register or update a student.
- [Organizations](https://hub.main-team.org/api/organizations): the core record, the five olympiads and `<organizationId>`.
- [Pagination](https://hub.main-team.org/api/pagination) and [Rate limits](https://hub.main-team.org/api/rate-limits).
