# Quickstart

> Make your first authenticated call to the Main Team API in about ten minutes, from credentials to a signed token to your organization ids.

This page takes you from "I have credentials" to "I have a working token and the ids I need for every other call". You will:

1. Keep your credentials on your server.
2. Sign a token (Node.js, PHP or bash).
3. Call `GET /v1/api-account/validate-me` to prove the token works.
4. Call `GET /v1/organization` to get the organization ids that every organization route needs.

Plan on about ten minutes. Every command below runs against production, `https://api.main-team.org/v1`, so read [Environments](https://hub.main-team.org/api/environments) before you write any data.

## Before you start

You need four things:

| You need | Why | Where it comes from |
|---|---|---|
| An `apiKey` | Names your account. It is not secret. | Issued by an operator, together with the secret |
| An `apiSecret` | Signs your tokens. It is secret and shown to you only once. | Issued by an operator |
| Roles on your account | Every route needs a permission. For this page you need `api/*` and `organization/read`, both on `mto`. | Set by an operator. See [Permissions](https://hub.main-team.org/api/permissions) |
| A server with a correct clock | Tokens carry timestamps, and the API allows only 30 seconds of clock difference | NTP on your server |

There is no public sign-up. Access is by arrangement: write to **info@main-team.org** to request an account. Say which organizations you work with and what your integration will do, so the operator can give you the right roles from the start (the ready-made role profiles in [Permissions](https://hub.main-team.org/api/permissions#ready-made-role-profiles) are a good way to ask).

An `apiKey` looks like `key_7fQx2LmN9pRtVw3YzA1bC4dE`: `key_` followed by exactly 24 characters. An `apiSecret` starts with `secret_`.

The `apiSecret` is shown once, when the account is created. It cannot be recovered later, and there is no way to change it on an existing account. Put it in your secret store straight away. Never commit it, never put it in a browser or mobile app, and never paste it into a website, including online JWT debuggers.

## Step 1: Store your credentials

Keep both values in your server's environment or secret manager, never in source code. The examples on this page read them from two environment variables:

```bash
export MTO_API_KEY='key_7fQx2LmN9pRtVw3YzA1bC4dE'
export MTO_API_SECRET='secret_...'   # the full value, including the "secret_" prefix
export MTO_API_BASE='https://api.main-team.org/v1'
```

Use the secret exactly as it was issued: the whole string, `secret_` prefix included, with no trailing newline or spaces. If you read it from a file, trim the line ending. A secret with an extra newline signs tokens that the API refuses.

## Step 2: Sign a token

The API has no login endpoint. You sign your own short-lived token, a JSON Web Token (JWT), with your `apiSecret`, and send it as a bearer token. The token must have:

- the header `alg` set to `HS256` and `kid` set to your `apiKey`,
- the claim `sub` set to your `apiKey` (the same value as `kid`),
- the claims `iat` (issued at) and `exp` (expires), both in **seconds** since the Unix epoch, with `exp` at most **3600** seconds after `iat`.

[Authentication](https://hub.main-team.org/api/authentication) explains each rule, and has token caching and a full troubleshooting checklist. The shortest working versions are below.

### Node.js

```bash
npm install jsonwebtoken
```

```js
// mint-token.js
const jwt = require('jsonwebtoken');

const apiKey = process.env.MTO_API_KEY;
const apiSecret = process.env.MTO_API_SECRET;

const iat = Math.floor(Date.now() / 1000); // seconds, not milliseconds
const token = jwt.sign(
  { sub: apiKey, iat, exp: iat + 3600 },
  apiSecret,
  { algorithm: 'HS256', keyid: apiKey }, // keyid becomes the "kid" header
);

console.log(token);
```

```bash
export TOKEN=$(node mint-token.js)
```

### PHP

```bash
composer require firebase/php-jwt
```

```php
<?php
// mint-token.php
require __DIR__ . '/vendor/autoload.php';

use Firebase\JWT\JWT;

$apiKey = getenv('MTO_API_KEY');
$apiSecret = getenv('MTO_API_SECRET');

$iat = time();
$token = JWT::encode(
    ['sub' => $apiKey, 'iat' => $iat, 'exp' => $iat + 3600],
    $apiSecret,
    'HS256',
    $apiKey // the fourth argument becomes the "kid" header
);

echo $token, PHP_EOL;
```

```bash
export TOKEN=$(php mint-token.php)
```

### bash and openssl

This is handy for a first test from a terminal. For production code, use a JWT library.

```bash
b64url() { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }

now=$(date +%s)
header=$(printf '{"alg":"HS256","typ":"JWT","kid":"%s"}' "$MTO_API_KEY" | b64url)
payload=$(printf '{"sub":"%s","iat":%d,"exp":%d}' "$MTO_API_KEY" "$now" "$((now + 3600))" | b64url)
signature=$(printf '%s.%s' "$header" "$payload" \
  | openssl dgst -sha256 -hmac "$MTO_API_SECRET" -binary | b64url)

export TOKEN="$header.$payload.$signature"
```

## Step 3: Check your token with validate-me

`GET /v1/api-account/validate-me` returns the account your token belongs to. It is the simplest way to prove that your key, your secret, your signing code and your clock all work.

```bash
curl -i "$MTO_API_BASE/api-account/validate-me" \
  -H "Authorization: Bearer $TOKEN"
```

A working token gets `200`:

```http
HTTP/2 200
content-type: application/json; charset=utf-8
x-request-id: 3f2a9c1e-6b7d-4e8f-9a0b-1c2d3e4f5a6b
x-ratelimit-limit: 100
x-ratelimit-remaining: 99
x-ratelimit-reset: 60
```

```json
{
  "_id": "66f1a2b3c4d5e6f7a8b9c0d1",
  "apiKey": "key_7fQx2LmN9pRtVw3YzA1bC4dE",
  "companyName": "Northwind Learning Ltd",
  "scopes": [],
  "roles": [
    { "effect": "allow", "action": "api/*", "target": "mto" },
    { "effect": "allow", "action": "*/read", "target": "mto" },
    { "effect": "allow", "action": "student/*", "target": "mto" },
    { "effect": "allow", "action": "*/read", "target": "stem" },
    { "effect": "allow", "action": "application/*", "target": "stem" },
    { "effect": "allow", "action": "auth/signin", "target": "stem" }
  ],
  "isActive": true
}
```

Two things to notice:

- **This response has no envelope.** `validate-me` is the one JSON route that returns the object on its own. Every other JSON route wraps its payload in `{ "success", "message", "data" }`, which you will see in the next step. See [Requests and responses](https://hub.main-team.org/api/requests-and-responses).
- **`roles` is your permission list.** Read it now. If a later call answers `403`, this list is where you find out why. [Permissions](https://hub.main-team.org/api/permissions) explains how to read it.

The `X-Request-Id` header is on every response, successful or not. Keep it in your logs: if you contact support, it is the value that lets us find your request. Treat it as an opaque string. You can also send your own `X-Request-Id` (letters, digits and `._:;=+/@-`, at most 256 characters), and the API will use it. See [Environments](https://hub.main-team.org/api/environments#response-headers-to-know).

## Step 4: Get your organization ids

Most routes act on one organization and take its id in the path: `/v1/{organizationId}/...`. That id is the organization's `_id` from `GET /v1/organization`. **It is never the slug**, so `/v1/stem/exam` does not work.

```bash
curl "$MTO_API_BASE/organization" \
  -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": 20, "total": 5, "totalPages": 1 }
}
```

(Example data, shortened to two of the five organizations. `mto`, the core record, is not listed:
the routes without an organization id act on it.)

Build a slug-to-id map once and keep it. Organizations rarely change, so caching the map for a day is fine. The list is not filtered by your roles, so it can include organizations you cannot act on; use the ones you agreed with the operator.

```js
const res = await fetch(`${process.env.MTO_API_BASE}/organization?limit=100`, {
  headers: { Authorization: `Bearer ${token}` },
});
const { data } = await res.json();
const orgIds = Object.fromEntries(data.map((o) => [o.slug, o._id]));
// orgIds.stem === '64b7f0c2a1e4d5f6a7b8c9d1'
```

```php
$ch = curl_init(getenv('MTO_API_BASE') . '/organization?limit=100');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
]);
$body = json_decode(curl_exec($ch), true);
$orgIds = array_column($body['data'], '_id', 'slug');
// $orgIds['stem'] === '64b7f0c2a1e4d5f6a7b8c9d1'
```

[Organizations](https://hub.main-team.org/api/organizations) explains which data lives on the core record (`mto`) and which lives in each organization, and why most routes need this id.

### A mistake worth seeing once

Using the slug where the id belongs is the most common first mistake. The API answers it like an organization that does not exist:

```bash
curl "$MTO_API_BASE/stem/exam" -H "Authorization: Bearer $TOKEN"
```

```json
{
  "error": {
    "code": "not_found",
    "message": "Organization not found!",
    "documentation_url": "https://hub.main-team.org/api/errors#not_found",
    "request_id": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f"
  }
}
```

Put `64b7f0c2a1e4d5f6a7b8c9d1` (the `_id`) where `stem` is and the same request reaches the stem organization.

## If your first call fails

Every error has the same shape: `{ "error": { "code", "message", "documentation_url", "request_id" } }`. Base your code on the HTTP status and `error.code`, not on the message text.

### Status 401: unauthorized

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Authentication is required or the provided credentials are invalid.",
    "documentation_url": "https://hub.main-team.org/api/errors#unauthorized",
    "request_id": "0b5c6d0e-8f7a-4b1c-9d2e-3f4a5b6c7d8e"
  }
}
```

Every authentication failure gets this exact response, whatever the cause. That is deliberate: someone holding a stolen key or token learns nothing from it. So work through the list yourself:

1. The header is exactly `Authorization: Bearer <token>`: capital `B`, one space, no quotes.
2. The token's header has `"alg": "HS256"` and `"kid"` equal to your `apiKey`, character for character.
3. The payload has `"sub"` equal to the same `apiKey`.
4. `iat` and `exp` are both present and are whole **seconds**. Milliseconds (`Date.now()` in JavaScript) are the classic mistake.
5. `exp` is later than `iat`, and at most 3600 seconds later.
6. Your server clock is within 30 seconds of real time.
7. You signed with the full secret, `secret_` prefix included, with no trailing newline.
8. The token has not expired and was not revoked, and your account has not been deactivated.

[Authentication](https://hub.main-team.org/api/authentication#the-401-checklist) goes through each item in detail, with a way to decode your token locally.

### Status 403: forbidden

```json
{
  "error": {
    "code": "forbidden",
    "message": "Insufficient role permissions",
    "documentation_url": "https://hub.main-team.org/api/errors#forbidden",
    "request_id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9"
  }
}
```

Your token is fine, but your account has no role for this route. On this page that means one of these is missing:

| Call | Needs | On |
|---|---|---|
| `GET /v1/api-account/validate-me` | `api/*` (only `api/*`, `*/*` or `*` grant it; `*/read` does not) | `mto` or `*` |
| `GET /v1/organization` | `organization/read` (also granted by `*/read`, `organization/*`, `*/*` or `*`) | `mto` or `*` |

A new token or a retry does not help. Ask the operator for the role, and see [Permissions](https://hub.main-team.org/api/permissions) for what to ask for.

### Other answers

| Status | Code | What it means here |
|---|---|---|
| 404 | `not_found` | `Organization not found!`: the path has a slug or an unknown id where the organization `_id` belongs |
| 429 | `too_many_requests` | You sent more than 100 requests to one route within 60 seconds. Wait the number of seconds in the `Retry-After` header. See [Rate limits](https://hub.main-team.org/api/rate-limits) |
| 5xx | `internal_error` | A problem on our side. Retry later, and quote the `request_id` if it persists |

## Next steps

- Build token handling properly: [Authentication](https://hub.main-team.org/api/authentication) and the [token handling tutorial](https://hub.main-team.org/api/tutorials/token-handling).
- Learn where data lives: [Organizations](https://hub.main-team.org/api/organizations).
- Check what your roles allow: [Permissions](https://hub.main-team.org/api/permissions).
- Register a student and enter them for an exam, end to end: [Register and apply](https://hub.main-team.org/api/tutorials/register-and-apply).
- Prefer a ready-made client? See [Client libraries](https://hub.main-team.org/api/clients).
