# Frequently asked questions

> Short answers to the questions partners ask most, grouped by topic, each with a link to the page that covers it in full.

Short answers, grouped by topic. Each answer links to the page that explains the subject in full. If your question is really "why did I get this error?", go to [Troubleshooting](https://hub.main-team.org/api/troubleshooting), which is organized by symptom.

The examples use `$TOKEN` for a token you have signed (see [Authentication](https://hub.main-team.org/api/authentication)), `<organizationId>` for an organization's `_id`, and `<studentId>` for a student's `_id`. Every example calls production, `https://api.main-team.org/v1`.

## Access

### How do I get API credentials?

Access is by arrangement. You cannot sign up for it yourself. Write to [info@main-team.org](mailto:info@main-team.org). An operator then creates an API account for your organization and gives you two values: an `apiKey` (public, starts with `key_`) and an `apiSecret` (private, starts with `secret_`). The same operator sets your account's roles, which decide which operations you may call. See [Quickstart](https://hub.main-team.org/api/quickstart) for your first call.

### I lost my apiSecret. Can you send it again?

No. The secret is shown once, when the account is created, and nobody can show it to you again. The only way forward is a new account. Before you accept one, read the next two answers: a new account cannot see the students your old one registered.

### Can I rotate my secret?

Not today. Secrets cannot be rotated in place. If your secret has leaked, ask [support](https://hub.main-team.org/api/support) to deactivate the account. Within a minute, every token signed with that secret stops working. Then agree the next step with the operator. A replacement account is a different account and does not own your existing students, so plan how you will carry on before switching.

If you only suspect that one token leaked, and not the secret, revoke that token with [`POST /v1/api-account/revoke-token`](https://hub.main-team.org/api/reference/revoke-token). Your secret and your other tokens keep working.

### Why can't my new account see the students my old account registered?

Every student belongs to the API account that registered them, and the API only ever shows an account its own students. To any other account, even a replacement account for the same company, those students answer exactly like records that do not exist. This rule is what keeps partners' data apart. See [Organizations](https://hub.main-team.org/api/organizations).

### Which permissions do I need?

Every operation needs one permission, and its reference page names it. One case needs two: registering a student with a `password` also needs `auth/signin` on `mto`. For example, [`POST /v1/{organizationId}/application`](https://hub.main-team.org/api/reference/create-application) needs `application/create` on that organization. Operations without an organization in their path need the permission on `mto`. [Permissions](https://hub.main-team.org/api/permissions) has ready-made role sets for common integrations. Tell the operator which profile you need.

### My token works, but every call answers 403. Why?

`403 forbidden` means your token is valid but your roles do not grant this operation. Three cases are common:

- The two operations under `/v1/api-account` need the action `api/*` (or `*/*` or `*`). A role for `*/read` does not grant them.
- A flat operation, such as `GET /v1/student`, needs a role whose target is `mto` or `*`. A role on `stem` does not reach it.
- Sign-in links and passwords need `auth/signin`. No `student/*` role grants it.

Only an operator can change roles, and a change takes effect within a minute. See [Troubleshooting](https://hub.main-team.org/api/troubleshooting#status-403-forbidden).

### Can my account be limited to some organizations?

Yes. Every role names a target: one organization slug, or `*` for all of them. An account whose roles target only `stem` can act on stem and on nothing else. You still need roles on `mto` for the flat operations: registering students, reading reference data, and checking your own account.

### Can several of our servers share one account?

Yes. Each server can sign its own tokens with the same key and secret. Keep in mind that the rate limit belongs to the account and not to the server. Ten servers share the same 100 requests per minute for each operation. See [Rate limits](https://hub.main-team.org/api/rate-limits).

## Authentication

### Why do I always get 401?

Every authentication failure answers with the same `401 unauthorized` and the same message, so check your token against the rules:

- The header is exactly `Authorization: Bearer <token>`, with a capital `B` and one space.
- The token is signed with HS256 using your `apiSecret`.
- The `kid` header is your `apiKey`, exactly as issued.
- `sub` equals `kid`.
- `iat` and `exp` are both present, as whole seconds, and `exp - iat` is at most 3600.
- Your server's clock is within 30 seconds of the correct time.
- The token has not expired or been revoked, and your account is active.

[Troubleshooting](https://hub.main-team.org/api/troubleshooting#status-401-unauthorized) has a script that decodes your token and checks each rule.

### Why doesn't the 401 say what is wrong?

On purpose. If the message named the failed check, anyone holding a leaked key or token could learn whether it still works. The reason is recorded against the request id. If the checklist does not find the problem, send the `request_id` from the error to [support](https://hub.main-team.org/api/support), and we can tell you which check failed.

### How long can a token live?

At most **3600 seconds** (one hour) from `iat` to `exp`. A token with a longer lifetime is refused, even if it has not expired yet. Tokens of 5 to 60 minutes work well.

### Do I need a new token for every request?

No. Sign one token and reuse it for every request until shortly before it expires. For example, replace it 60 seconds before `exp`. [Token handling](https://hub.main-team.org/api/tutorials/token-handling) shows a small cache in Node.js and PHP.

### Is there a token endpoint or an OAuth flow?

No. You sign the token yourself, on your own server, with any JWT library that supports HS256. There are no keypairs, certificates or redirects involved. The API never sees your secret in a request.

### How do I check that my credentials work?

Call [`GET /v1/api-account/validate-me`](https://hub.main-team.org/api/reference/get-current-api-account). It returns your account, including the roles you hold.

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

```json
{
  "_id": "66f1a2b3c4d5e6f708192a3b",
  "apiKey": "key_Q2x5c3RhbGxpbmVfZXhhbXBs",
  "companyName": "Example Schools Ltd",
  "scopes": [],
  "roles": [
    { "effect": "allow", "action": "*/read", "target": "*" },
    { "effect": "allow", "action": "api/*", "target": "mto" }
  ],
  "isActive": true
}
```

### Why is validate-me's response not wrapped in `{ success, data }`?

It is the one JSON response without the envelope: it returns your account object directly. Every other JSON response uses `{ success, message, data }`. Your client should handle this operation separately. See [Requests and responses](https://hub.main-team.org/api/requests-and-responses).

### How do I end a token early?

Call [`POST /v1/api-account/revoke-token`](https://hub.main-team.org/api/reference/revoke-token) with the token you want to end in the `Authorization` header. Your account needs `api/*` on `mto` for this. From then on, the API refuses that token. `expiresIn` in the response tells you how many seconds the API keeps refusing it, which is until the token would have expired anyway.

```bash
curl -s -X POST https://api.main-team.org/v1/api-account/revoke-token \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "success": true,
  "message": "Token revoked successfully",
  "data": { "expiresIn": 1834 }
}
```

### Our server clock is a little off. Does that matter?

Up to 30 seconds does not. The API accepts a token for 30 seconds after its `exp`, and it accepts an `iat` up to 30 seconds in the future. Beyond that, a clock that runs fast produces tokens whose `iat` is too far ahead, and a clock that runs slow produces tokens that have already expired. Both are refused with `401`. Keep your servers synchronized with NTP.

### Can we call the API from a browser or a mobile app?

No. Call it only from your servers. A token can only be signed with your `apiSecret`, and any secret shipped to a browser or an app belongs to everyone who installs it. The production API also sends no CORS headers, so browsers block cross-origin calls to it. To send a student into the student panel, let your server create a [sign-in link](https://hub.main-team.org/api/guides/sign-in-links) and redirect the student's browser to it. See [Security](https://hub.main-team.org/api/security).

## Students

### What do I need to register a student?

A `POST /v1/student` with at least these fields:

| Field | Format |
|---|---|
| `firstName` | Text, required |
| `lastName` | Text, required |
| `email` | A valid address, required |
| `birth` | `DD/MM/YYYY` and a date that exists, required, for example `14/05/2011` |
| `sex` | `m`, `f` or `n`, required |
| `country` | A country `_id` from [`GET /v1/country`](https://hub.main-team.org/api/reference/list-countries), required |
| `grade` | A grade `_id` from [`GET /v1/grade`](https://hub.main-team.org/api/reference/list-grades), or its name (`"1"` to `"12"`), required |
| `city` | A city `_id` or its name, required |
| `school` | A school `_id` or its name, required |

`phone` is optional. The full field list, including `password` and `activatedPlatformsThisSeason`, is in [Students](https://hub.main-team.org/api/guides/students) and on [`registerStudent`](https://hub.main-team.org/api/reference/register-student).

### Registration answers "A student with that email is already registered to this account". What now?

You registered that address before. The message ends with that student's `_id`. Fetch the student with [`GET /v1/student/{studentId}`](https://hub.main-team.org/api/reference/get-student), or update them with [`PUT /v1/student/{studentId}`](https://hub.main-team.org/api/reference/update-student). Registration is not idempotent: a repeated request never returns the existing student with `201`.

### Registration answers "That email address is already registered." with no id. Why?

An email address can belong to only one person on the whole platform. This address is already used by a student another partner registered, or by someone who signed up on their own. The API does not say who holds it, and it cannot attach an existing account to yours. An update that moves a student onto an address already in use gets the same answer. Register the student with a different address, or contact [support](https://hub.main-team.org/api/support) if you believe the address is wrongly taken.

### Can I send the grade, city or school as a name?

Yes. `grade` accepts the grade's `_id` or its name, which is `"1"` to `"12"`. `city` and `school` accept an `_id` or a name. A city name is looked up within the country you send, and a school name within that country and city. Names are trimmed and matched without regard to case. `country` accepts only an `_id`. A value that matches nothing is refused with `400`. The API never creates a city, school or grade for you. See [Reference data](https://hub.main-team.org/api/guides/reference-data).

### Why is a country missing from `GET /v1/country`, or refused at registration?

Some countries cannot be selected. They are left out of the list, and sending one of their ids is refused with `400 country is not a known country.`, the same answer as an id that does not exist. Use only ids from the list.

### How are usernames made, and can I choose one?

You cannot choose one. Registration issues the username, and the API never changes it. It is the country's two-letter code, one letter, and a sequence number, for example `XXB1045` with the country's code in place of `XX`. Usernames are not secret: students and support staff use them to identify an account.

### Should I set students' passwords?

Prefer [sign-in links](https://hub.main-team.org/api/guides/sign-in-links). A student who arrives through a link needs no password. If you do set one, the rules are:

- It must be at least 5 characters and at most 72 bytes.
- It must not contain the student's first name, surname, username, email address, or the part of the address before the `@`. Case does not matter, and only parts of 4 or more characters count, so a two-letter surname rules nothing out.
- Your account needs the `auth/signin` permission on `mto`.
- Once the student has confirmed their own email address, you can no longer set it. The request answers `409`, because the account now belongs to the student.

Generate a random password for each student. Never derive one from the student's record. See [Passwords](https://hub.main-team.org/api/guides/passwords).

### An update answers "property … should not exist". Why?

The API refuses any field it does not accept, rather than silently ignoring it. That way you never receive a success for a request that did less than you asked. The usual cause is sending a whole record back, including fields such as `_id`, `username`, `fullName`, `emailConfirmed` or `createdAt`. Send only the fields you want to change. `password` is refused on both update operations. It has [its own operation](https://hub.main-team.org/api/reference/set-student-password).

### What is the difference between `/v1/student` and `/v1/{organizationId}/student`?

Both work on the same student record.

- `/v1/student` operations act on the core record. They are where you register students, change their profile and set passwords.
- `/v1/{organizationId}/student` operations show the same record from one organization's point of view. The list includes only students with access to that organization, and an update there also gives the student access to it, unless the body sends `activatedPlatformsThisSeason`, whose values are added instead.

Linking a supervisor exists only in the organization form, because a supervisor belongs to one organization. See [Students](https://hub.main-team.org/api/guides/students).

### `GET /v1/student/{studentId}` answers 404 for a student I know exists. Why?

`404 not_found` with `Student not found!` means the student is not one your account can see. The student does not exist, or another account registered them, or, on the organization form, [`GET /v1/{organizationId}/student/{studentId}`](https://hub.main-team.org/api/reference/get-org-student), the student has no access to that organization. The API answers these cases alike on purpose. Check that you are using the id registration returned, with a token for the account that registered the student.

### How do I control which organizations a student can use?

With `activatedPlatformsThisSeason`. At registration it defaults to `["common"]`, which means every organization. To limit the student, send a list of slugs, for example `["stem", "neo"]`. The slugs are `stem`, `hilingua`, `neo`, `gmath` and `coding`.

After registration the list only grows. Neither update removes an organization from it:

- [`PUT /v1/{organizationId}/student/{studentId}`](https://hub.main-team.org/api/reference/update-org-student) without the field, even with an empty body `{}`, adds that organization, unless the list already holds `common`.
- Either update with the field adds the values you send. A value the list already holds is not added twice, and `[]` adds nothing.

So register a student with a narrow list only if they should stay off the other organizations.

### How do I link a student to a supervisor, and why do I get "Not found!"?

Send the supervisor's username to [`PUT /v1/{organizationId}/student/{studentId}/supervisor`](https://hub.main-team.org/api/reference/link-student-supervisor):

```bash
curl -s -X PUT "https://api.main-team.org/v1/<organizationId>/student/<studentId>/supervisor" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"supervisorUsername": "XXT1003"}'
```

Every refusal answers the same `404 Not found!`, so check each of these:

- The student is yours.
- The student has signed in to that organization at least once.
- The username exists. Spaces around it are removed and case does not matter, but a username made only of spaces answers `404` too.
- The account behind the username is a supervisor on that organization.

The link applies to that organization only. It is not shown by the student read operations, which return the core record. Keep the link call's response if you need it. See [Supervisors](https://hub.main-team.org/api/guides/supervisors).

### Can I delete a student?

Not through the API in version 1. Contact [support](https://hub.main-team.org/api/support) if a student record must be removed.

## Exams and applications

### Why doesn't an exam appear in `GET /v1/{organizationId}/exam`?

That operation lists only exams that are open for applications. An exam is open when all of these hold:

- Its session date is in the future.
- Its category is active.
- Applications to it are not switched off.

A closed exam is also refused by its single read ([`getExam`](https://hub.main-team.org/api/reference/get-exam)), which answers `404`, and by applications. The list is not a full catalog. See [Exams](https://hub.main-team.org/api/guides/exams).

### Why is an exam missing from a student's available exams?

[`GET /v1/{organizationId}/exam/available/{studentId}`](https://hub.main-team.org/api/reference/list-available-exams) offers only exams the student may actually take. An open exam is missing when at least one of these is true:

- It does not accept the student's grade.
- It is restricted to countries that do not include the student's country.
- It has no language set.
- The student already holds an exam in the same category on the same session.

The last rule is why a student entered for Science on one date is not offered Science in another language on that same date.

The country rule has one more edge. The student's country is matched by name in the organization's own country list. If the organization has no country of that name, the student is offered only exams that have no country restriction.

### The available exams list answers "Student has no grade set". What do I do?

Every exam accepts a fixed set of grades, so a student without a grade cannot be offered anything. Set one, then list again:

```bash
curl -s -X PUT "https://api.main-team.org/v1/student/<studentId>" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"grade": "7"}'
```

### How do I apply a student for an exam?

1. List the student's available exams.
2. Pick a leaf of the tree.
3. Send its `matchedExam._id` together with the student's id to [`POST /v1/{organizationId}/application`](https://hub.main-team.org/api/reference/create-application).

```bash
curl -s -X POST "https://api.main-team.org/v1/<organizationId>/application" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"studentId": "<studentId>", "examId": "<examId>"}'
```

[Register and apply](https://hub.main-team.org/api/tutorials/register-and-apply) walks through the whole flow.

### Applying answers "Student has never signed in to …". Why?

Applications live on the organization's own copy of the student, and that copy is created the first time the student opens a [sign-in link](https://hub.main-team.org/api/guides/sign-in-links) for that organization. Creating the link is not enough: the student's browser has to open it. Until then, applications, the student's application list, certificates, reports and supervisor links on that organization all refuse. The refusal is `409` for most of them and `404` for the supervisor link.

### Is it safe to retry `POST /v1/{organizationId}/application`?

Yes, when you retry with the same student and exam. If the application already exists, the API answers `200` with the message `Application already exists.` and the existing application. A new application answers `201`. A different exam in the same category on the same session is refused with `409`.

The exam is checked before the API looks for an existing application. Once the exam has closed, a retry answers `409` even though the application exists, so look for it in the student's application list instead. See [Retries and idempotency](https://hub.main-team.org/api/retries-and-idempotency).

### Can I change an application's language or date?

Yes. Move it to the exam you want with [`PUT /v1/{organizationId}/application/{applicationId}`](https://hub.main-team.org/api/reference/move-application), sending `{"examId": "<examId>"}`. The new exam has to be one the student's available exams list would offer. The student's current application does not count against the move, so switching language within the same session works. See [Change an application](https://hub.main-team.org/api/tutorials/change-an-application).

### Can I move a paid application?

Yes, if the new exam costs the same. The API checks a move in this order and answers `409` for the first rule it breaks:

1. The student has already started or submitted the exam.
2. The new exam is not one the student's available exams would offer: it is closed, not for the student's grade or country, or has no language. A student with no grade gets `400` here instead.
3. The old exam's category does not allow a switch to the new one.
4. The student already holds another exam in the new exam's category on the same session.
5. The application is for the AI Challenge and part of its image quota has been used. This applies whether or not the application was paid for.
6. The application has been paid for and the new exam has a different price. The API can neither charge the difference nor refund it.

Two other answers are not refusals. An `examId` that names no exam on that organization answers `404 Exam not found!`. The exam the application already uses answers `200` with `Application already uses that exam.` and changes nothing.

A payment that has not been made yet follows the new exam's price automatically. It becomes paid with an amount of `0` if the new exam is free, and pending otherwise.

### Can I delete an application?

Yes, unless it has been paid for. [`DELETE /v1/{organizationId}/application/{applicationId}`](https://hub.main-team.org/api/reference/delete-application) refuses an application with a settled payment with `409`, because cancelling a paid sitting needs a refund, and the API cannot make one. An application for a free exam is not "paid" in this sense and can be deleted. Deleting the same application a second time answers `404`.

### Does the API take payments or issue refunds?

No. When you create an application, the API also records its payment at the exam's price:

- For a free exam, the payment is recorded as paid with an amount of `0`.
- For a priced exam, the payment is recorded as pending. It is settled outside the API.

The API never charges or refunds anyone. In exam responses, `price` is left out for an exam nobody has priced. The API does not fill in `0` there, so write `price ?? 0` in your own code if you need a number. When a student applies for such an exam, the payment is recorded as it is for a free exam: paid, with an amount of `0`.

## Sign-in links

### How long is a sign-in link valid, and how often can it be used?

For **120 seconds**, and **once**. Create it when the student clicks, then redirect their browser to it immediately. Never store a link, send it by email, or create links in advance. See [Sign-in links](https://hub.main-team.org/api/guides/sign-in-links) and [Send a student to the panel](https://hub.main-team.org/api/tutorials/send-student-to-panel).

### The student sees "Invalid or expired access token". Why?

The link had already been used, or it was more than 120 seconds old. A common hidden cause is a program that opened the link before the student did: a chat or email link preview, a security scanner, or browser prefetching. Any of these uses up the link. Create a fresh link at the moment of the click and answer with a `302` redirect, so the URL never appears anywhere a preview could fetch it.

### The student has not confirmed their email address. Can they still sign in?

Yes. A sign-in link does not require a confirmed address. Students you register start out unconfirmed and can use a link straight away. Once they land, the panel asks them to confirm the address with a 6-digit code it emails to them, and asks on every page until they do. They can't put it off: the only way out is signing out. The prompt covers My Exams too, so they must confirm before they can start an exam. A student already inside an exam room is not interrupted. Have your students confirm well before an exam day, for example right after their first sign-in. A code is valid for 15 minutes, and they can ask for a new one after 60 seconds. See [Email confirmation](https://hub.main-team.org/api/guides/sign-in-links#email-confirmation).

### Creating a link answers 403 "Student is not activated for organization …". What do I do?

The student has no access to that organization. Their `activatedPlatformsThisSeason` holds neither `common` nor this organization's slug. Give them access with [`PUT /v1/{organizationId}/student/{studentId}`](https://hub.main-team.org/api/reference/update-org-student), then create the link again. See [How do I control which organizations a student can use?](#how-do-i-control-which-organizations-a-student-can-use)

### Where does the student land?

On the organization's home page, unless you send `redirect`, a path inside the organization such as `/dashboard`. The path must start with a single `/` (`//` is refused) and contain no whitespace and no backslashes. The text `{userId}` in the path is replaced with the student's id on that organization. A full URL is refused with `400`, which means a link can never send the student to another site.

## Results

### Why is a student's certificate or report list empty, or why does a download answer 404?

The API lists and serves only documents the organization has released. Results under embargo are not visible until they are published. A download answers the same `404 Not found!` in each of these cases:

- The id is wrong.
- The document is not released yet.
- The document belongs to another account's student.
- The document has no file.

Listing a student's documents also requires the student to have signed in to that organization at least once. See [Certificates and reports](https://hub.main-team.org/api/guides/certificates-and-reports).

### Can I download a document by its short id?

Yes. [`downloadCertificate`](https://hub.main-team.org/api/reference/download-certificate) and [`downloadReport`](https://hub.main-team.org/api/reference/download-report) accept either the document's `_id` or its `shortId`.

### What file name should I save a download under?

Use the name in the `Content-Disposition` header. Prefer its `filename*=UTF-8''…` part, which carries the real name, including non-English letters. Fall back to `filename="…"` only when `filename*` is missing.

### How often should we check for new results?

Once a day is enough for most integrations, because results are published in batches. [Collect results](https://hub.main-team.org/api/tutorials/collect-results) shows a nightly job that stays within the rate limit and skips documents you already have.

## Limits and environments

### What are the rate limits?

Each API account may make **100 requests per 60 seconds to each operation**. The same operation on different organizations shares one budget, and two different operations never do. Past the limit, the API answers `429 too_many_requests` with a `Retry-After` header that gives the number of seconds to wait. Counted responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. See [Rate limits](https://hub.main-team.org/api/rate-limits).

### How large can a request be?

A JSON body may be at most **100 kB**. A larger body is refused with `413 payload_too_large` before anything else is checked. The largest body the API expects is one student registration, which is far smaller.

### Is there a sandbox?

Yes, at `https://apisnd.main-team.org/v1`. It runs the same release as production and has:

- separate accounts, issued by an operator from the sandbox's own panel (ask [support](https://hub.main-team.org/api/support))
- seeded reference data, with the same organization ids as production
- no emails sent and no real payments

Registration is open there, with the same permissions and rate limits as in production. The official client libraries accept only the production base URL, so call the sandbox over HTTPS directly. See [Environments](https://hub.main-team.org/api/environments#sandbox).

### Can I use "Try it" in the reference?

Yes, against the sandbox only. Sign a token with your sandbox credentials on your own machine and paste the token, never your `apiSecret`. The console never sends requests to production. See [Environments](https://hub.main-team.org/api/environments#sandbox).

### Is there a health or status endpoint?

[`GET /v1/health`](https://hub.main-team.org/api/reference/get-health) needs no token and doesn't count against your account's rate limit. It answers `200` while the API is up:

```json
{
  "success": true,
  "message": "Request completed successfully.",
  "data": { "status": "ok" }
}
```

It only says that the service is running. To check your integration end to end, including your token and roles, call [`GET /v1/api-account/validate-me`](https://hub.main-team.org/api/reference/get-current-api-account).

### How will we hear about changes?

Through the [changelog](https://hub.main-team.org/api/changelog), which lists every version and what changed in it. The version these pages describe is shown in the [API reference](https://hub.main-team.org/api/reference). Every path stays under `/v1` for the whole of version 1. Additive changes, such as new operations, new optional fields and new response fields, can arrive at any time, so your code should ignore fields it does not know.

Before we deprecate anything, we announce it in the changelog at least 6 months ahead. The affected responses also carry `Deprecation` and `Sunset` headers. Removals happen only in a new major version. See [Versioning](https://hub.main-team.org/api/versioning).

### Are there client libraries?

Yes, for Node.js and PHP. They are private, and access comes with your credentials. Every operation is also plain HTTP with JSON, so you can use any HTTP client instead. See [Client libraries](https://hub.main-team.org/api/clients).
