# Client libraries

> The official Node.js and PHP clients for the Main Team API, how to get access, and when to call the API over plain HTTP instead.

There are two official clients for the Main Team API, one for Node.js and one for PHP. You don't
need either of them. Every operation is plain HTTPS with a JSON envelope, and the
[API reference](https://hub.main-team.org/api/reference) describes all of it. A client handles four things you would
otherwise write yourself: signing tokens, unwrapping the response envelope, turning error
responses into typed exceptions, and refusing calls that can't succeed before they are sent.

This page tells you what the clients do, how to get them, and when to skip them. The rules
at the end apply whether or not you use a client, so read them either way.

## The packages

| Language | Package | Install | Needs | Status |
| --- | --- | --- | --- | --- |
| Node.js / TypeScript | `@main-team/api-client` | `npm install @main-team/api-client` | Node.js 20 or newer | available |
| PHP | `main-team/api-client` | `composer require main-team/api-client` | PHP 8.1 or newer, `ext-curl`, `ext-json` | available |

- [Node.js client](https://hub.main-team.org/api/clients/node): ESM, CommonJS and TypeScript types in one package, with no
  runtime dependencies.
- [PHP client](https://hub.main-team.org/api/clients/php): no dependencies beyond two standard extensions, and a pluggable
  transport if you want PSR-18 or a proxy.
- [Build your own](https://hub.main-team.org/api/clients/build-your-own): what a client has to do, a minimal client in
  Node.js and in PHP, and a conformance checklist.

## Getting access

Both packages are **private**. Access is issued with your API credentials: the operator who gives
you your `apiKey` and `apiSecret` also gives you access to the packages. If you have credentials
but can't install a package, write to [info@main-team.org](mailto:info@main-team.org) and say
which package you need.

There is no self-service sign-up for the API or the packages. Accounts are created by an operator
by arrangement. The [Quickstart](https://hub.main-team.org/api/quickstart) explains what you receive and what to do with it.

## What a client does for you

| Concern | Without a client | With a client |
| --- | --- | --- |
| Authentication | You sign an HS256 token with your `apiSecret`, set `kid` and `sub` to your `apiKey`, and replace it before it expires. | You pass `apiKey` and `apiSecret` once. The client signs short-lived tokens for you. |
| Base URL | You join `https://api.main-team.org/v1` and the path. | Fixed. The client can't be pointed anywhere else (see below). |
| Organizations | You call `GET /v1/organization` and map slugs to `_id`s, because paths take the `_id` only. | You call `organization('stem')` and the client looks the `_id` up and caches it. |
| Response envelope | You read `data`, `message` and `pagination` out of `{ success, message, data, pagination? }`, and treat `validate-me` as the one JSON route without an envelope. | Methods return the data, or `{ data, pagination }` for lists. |
| Errors | You parse `{ error: { code, message, documentation_url, request_id } }`. | You catch typed exceptions that carry the same `code` and HTTP status. |
| Impossible calls | The server answers `400 bad_request`. | The client throws the same `bad_request` / 400 before sending, so one handler covers both. |
| Pagination | You loop `page` until `totalPages`. | A helper walks every page for you. |
| Downloads | You stream the PDF and read the file name from `Content-Disposition`. | You get a file object with the server's file name and the bytes. |

A client **does not** change the API's rules. It never invents a restriction the API doesn't
have, and it doesn't hide one the API does have. A registration with a duplicate email, an exam the
student can't take, or a paid application you try to delete all fail the same way through a
client as over HTTP. The difference is that you get a typed exception instead of a JSON body.

## Why the base URL is locked

Neither client has a `baseUrl` option. Every request carries a bearer token that is valid for your
whole account until it expires. If a client could be pointed at another host, by a typo, a
misread environment variable or a malicious configuration, that host would receive a token it
could replay as you. Fixing the host in the client removes that risk.

The sandbox, at `https://apisnd.main-team.org/v1`, has its own accounts, seeded reference data, no
emails sent and no real payments. The clients accept only the production base URL, so they can't
reach it. For the sandbox, call the API over HTTPS directly, or use a client of your own such as
the one in [Build your own client](https://hub.main-team.org/api/clients/build-your-own). See
[Environments](https://hub.main-team.org/api/environments#sandbox).

## Server-side only

Use the clients from your servers only. In production the API sends no CORS headers, so a web
page on another origin can't call it. That is deliberate: a browser page that could call the API
would have to carry your `apiSecret`, which would hand it to every visitor. The same applies to
mobile and desktop apps: anything you ship to a user can be unpacked.

If your users need to reach the student panel from your site, your server mints a
[sign-in link](https://hub.main-team.org/api/guides/sign-in-links) and redirects the browser to it. The
[Send a student to the panel](https://hub.main-team.org/api/tutorials/send-student-to-panel) tutorial shows how.

## Client or plain HTTP?

| Use a client when | Call the API directly when |
| --- | --- |
| Your backend is Node.js or PHP. | Your backend is another language: see [Build your own](https://hub.main-team.org/api/clients/build-your-own). |
| You want typed errors and pre-validated calls. | You already have an HTTP layer with retries, logging and metrics that you want to reuse. |
| You want organization slugs resolved for you. | You need a route before your package version supports it. |
| You'd rather not write token caching. | You're scripting a one-off with `curl` and `openssl`. |

Mixing both is fine. The API doesn't know or care which one sent a request.

## Keeping a client current

The clients carry the same list of routes as the API. When the API gains a route or a field, the
[changelog](https://hub.main-team.org/api/changelog) says so, and a client release follows. Two things to know:

- **Tolerate what you don't recognize.** New fields can appear in responses without notice, because
  adding a field is not a breaking change. The PHP client's models keep fields they don't model
  instead of failing on them, and your own code should do the same. See
  [Versioning](https://hub.main-team.org/api/versioning).
- **Watch for deprecations.** A route or field that is going away is announced at least 6 months
  ahead in the changelog, and responses from it carry `Deprecation` and `Sunset` headers. Removals
  happen only in a new major version of the API. The [changelog](https://hub.main-team.org/api/changelog) lists every
  version, and the [API reference](https://hub.main-team.org/api/reference) shows the one these pages describe.

## Rules that apply in any language

These are properties of the API, not of the clients. Each one links to the guide that covers it
in full.

### Every studentId is the core id

Each organization keeps its own copy of a student, under a different `_id`. Every `studentId` you
send, on every route, is the id that registration (`POST /v1/student`) returned. Never use an id
read from an organization's own records. The student on an application is that organization's
copy: its `_id` is the organization's id for them, and its `mainId` is the id you registered them
with. **Match on `mainId`.** See [Identifiers](https://hub.main-team.org/api/identifiers).

### A student signs in once before organization actions

Nothing in an organization refers to a student until that organization holds a copy of them, and
the copy is created the first time the student follows a [sign-in link](https://hub.main-team.org/api/guides/sign-in-links)
into that organization. Until then:

- Creating an application, or listing that student's applications, certificates or reports, answers
  `409 conflict`. The message tells you to send a sign-in link first.
- Linking a supervisor answers `404 not_found`, the one answer that route gives to every refusal.
- The organization's application lists leave the student out, without an error.

The organization's student list is different. It shows every student of yours who has access to
that organization, whether or not they have signed in there.

### Sign-in links are single-use and short-lived

`POST /v1/{organizationId}/auth/signin` returns a URL that signs the student in **once** and
expires **120 seconds** after it was issued. Redirect the browser to it immediately. Don't store
it, email it, log it or paste it into chat, where a link preview can use it up. Mint a new one
every time. A student whose email address isn't confirmed yet still gets a link. A student with no
access to that organization gets `403 forbidden` instead. The optional `redirect` must be a path on
the organization's site that starts with a single `/`, such as `/dashboard`. A full URL is refused
with `400 bad_request`.

### The exam listing is not a catalog

`GET /v1/{organizationId}/exam` returns only exams that are **open** for application. A closed
exam can't be read by id or applied to. To choose an exam for a particular student, use the
per-student picker, `GET /v1/{organizationId}/exam/available/{studentId}`. It applies that
student's grade and country and leaves out what they already hold. Every leaf of its tree carries a
`matchedExam`; send `matchedExam._id` as `examId`. See [Exams](https://hub.main-team.org/api/guides/exams).

### Applications are limited to what the picker offers

`POST /v1/{organizationId}/application` runs the same checks as the picker. An exam the picker
wouldn't offer is refused with `409 conflict`, and the message says which rule failed. Applying
twice to the same exam is safe: the second call answers `200` with `"Application already exists."`
and returns the existing application, where the first answered `201`. See
[Applications](https://hub.main-team.org/api/guides/applications).

### Registering the same email twice is a conflict

`POST /v1/student` is **not** idempotent. If the email is already registered to your account, you
get `409 conflict` and the message names the `_id` of the student who holds it. Fetch or update
that student instead of retrying. If a different account registered the address, you get the same
`409` without an id: email addresses are unique across the whole platform. A retry loop won't help
either way. See [Students](https://hub.main-team.org/api/guides/students).

### Unknown fields are errors

A request body with any property the API doesn't declare is refused with `400 bad_request`, and
the message names the property. Send the documented fields, not a whole record copied from your
own system. See [Requests and responses](https://hub.main-team.org/api/requests-and-responses).

### Passwords are optional, and need an extra permission

Only two routes set a password: registration (optional `password`) and
`PUT /v1/student/{studentId}/password`. Setting a password either way needs the `auth/signin`
permission on mto, the permission a sign-in link needs. A registration that carries a password
without that permission is refused with `403 forbidden`. A password must be at least 5 characters and at most 72 bytes,
and must not contain the student's name, username or email address. Once the student has confirmed
their email address, the password route answers `409 conflict`, and you send them a sign-in link
instead. See [Passwords](https://hub.main-team.org/api/guides/passwords).

### A 403 is a permission, not a token

`403 forbidden` means the token is fine but your account lacks the permission for that route on
that organization. Re-signing won't help. Ask your operator for the role. See
[Permissions](https://hub.main-team.org/api/permissions).

### A 429 means wait

Each account may make **100 requests per 60 seconds to each operation**, and each operation is
counted on its own. The count belongs to your
account, not to your IP address, so spreading calls over several servers doesn't raise it. After a
`429 too_many_requests`, wait the number of seconds in `Retry-After`. A request sent sooner is
refused too. See [Rate limits](https://hub.main-team.org/api/rate-limits).

## Getting help

When something fails, keep the `request_id` from the error body (it's also the `X-Request-Id`
response header) and the time in UTC. Send them to
[info@main-team.org](mailto:info@main-team.org) with the operation you called. Never send your
`apiSecret`, a token or a sign-in link. See [Support](https://hub.main-team.org/api/support) and
[Troubleshooting](https://hub.main-team.org/api/troubleshooting).
