Guides
Reference data
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 | List countries, Get a country |
| Grades | The grade field at registration, and the grade an exam is restricted to | List grades, Get a grade |
| Organizations | The <organizationId> in every /v1/<organizationId>/… path | List organizations, Get an 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).
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 and the message
Insufficient role permissions. The Permissions page explains how roles match.
Countries
List countries
curl "https://api.main-team.org/v1/country?page=1&limit=100" \
-H "Authorization: Bearer $TOKEN"
{
"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).
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.
Note
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
curl "https://api.main-team.org/v1/country/630e0182c53dc79a6836e67e" \
-H "Authorization: Bearer $TOKEN"
{
"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
curl "https://api.main-team.org/v1/grade?limit=100" \
-H "Authorization: Bearer $TOKEN"
{
"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
curl "https://api.main-team.org/v1/grade/630e01826836e67ec53dc79d" \
-H "Authorization: Bearer $TOKEN"
{
"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
curl "https://api.main-team.org/v1/organization" \
-H "Authorization: Bearer $TOKEN"
{
"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> 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 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 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.
{
"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 explains what the core record
and the five olympiads are, and why the flat routes act on mto.
Get one organization
curl "https://api.main-team.org/v1/organization/64b7f0c2a1e4d93b5c2f1a02" \
-H "Authorization: Bearer $TOKEN"
{
"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, and
every 400 the code 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 with the country, city and school as your
records have them. Students describes the lookup
rules in full.
Caching
Reference data changes rarely, and each read counts against your rate limit (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.orgrade 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.
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
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: how
country,grade,cityandschoolare resolved when you register or update a student. - Organizations: the core record, the five olympiads and
<organizationId>. - Pagination and Rate limits.