# Versioning and deprecation

> How the API is versioned, what counts as a breaking change, how deprecations are announced, and how to write a client that keeps working.

You build against the API once and expect it to keep working. This page explains the promises that make that possible: which changes can happen without notice, which ones can't, how you'll hear about them, and what your client has to do on its side.

## Two version numbers

| | Where you see it | What it tells you |
|---|---|---|
| **Major version** | The URL: `https://api.main-team.org/v1/...` | The contract you call. Every operation is under `/v1`. A breaking change needs a new major version, a new URL prefix such as `/v2`. |
| **Documented version** | The version badge on these pages, and the [changelog](https://hub.main-team.org/api/changelog) | The exact behavior these pages describe, as semantic versioning: `MAJOR.MINOR.PATCH`. |

The [API reference](https://hub.main-team.org/api/reference) shows the documented version these pages describe. The [changelog](https://hub.main-team.org/api/changelog) lists every version and what changed in it; the first, 1.0.0, also lists [the behaviors the API started with](https://hub.main-team.org/api/changelog/1-0-0).

The documented version moves like this:

- **Patch** (`1.0.0` → `1.0.1`): a fix or clarification that changes nothing your client can rely on.
- **Minor** (`1.0.x` → `1.1.0`): something new that is backward compatible, such as a new operation or a new response field.
- **Major** (`1.x` → `2.0.0`): a breaking change. It comes with a new URL prefix, and `/v1` keeps working alongside it (see [Deprecation and removal](#deprecation-and-removal)).

Put the base URL, `/v1` included, in one place in your configuration. The API sends no version header, and it doesn't need one: the path says which contract you are calling.

## What we will change without notice

These changes are backward compatible. They can arrive in any minor or patch version, and your client must not break when they do:

- **New operations**, on new paths.
- **New fields in responses**, at any level of any object.
- **New optional fields in requests.** Until you send them, nothing changes for you.
- **New values in a response field that already has a fixed set of values.**
- **New error codes**, for situations that didn't have their own code before.
- **New response headers.**
- **Different wording in `message` fields**, on successes and on errors.
- **A different order** of fields in an object, or of items in a list you didn't ask to have sorted.
- **More organizations** in `GET /v1/organization`, and new countries, grades, exam categories and exams in the reference data.
- **Relaxed validation**, meaning a request that used to be refused is now accepted.

## What counts as breaking

These changes need a new major version:

- Removing or renaming an operation, a path, a request field or a response field.
- Changing a field's type or format, for example turning a string into an object, or changing the `DD/MM/YYYY` birth-date format.
- Making an optional request field required, or adding a new required field.
- Changing the status code or the `error.code` the API gives for an existing situation.
- Requiring a different permission for an existing operation.
- Changing the success or error envelope.
- Changing what an operation does in a way that makes a correct client wrong, for example making `POST /v1/{organizationId}/application` stop being idempotent.

## Build a tolerant client

The API is strict about what it accepts: it refuses any request field it doesn't know with `400 bad_request`. Your client should be the opposite about what it receives. That asymmetry is what lets the API add things without breaking you.

**Ignore what you don't recognize.** Read the fields you need and ignore the rest. Don't validate responses against a closed schema that fails on an unknown field. That goes for errors too: `error` may carry an optional `details` object, and only where the operation says so. Today one does — [createStudentImport](https://hub.main-team.org/api/reference/create-student-import) puts the rows it cannot register there. Treat a `details` you do not recognize as absent.

**Handle unknown values.** When a field holds a value you've never seen, fall back to a safe default. Don't throw.

**Branch on status and `error.code`, never on `message`.** Messages are for people and may be reworded at any time. The `code` values are stable identifiers. If you meet a `code` your client doesn't know, handle it by its status class:

```js
function classify(status, error) {
  switch (error?.code) {
    case 'unauthorized':
      return 'refresh-token';
    case 'too_many_requests':
      return 'wait';
    case 'conflict':
      return 'check-state';
    // ...the codes you handle specially
    default:
      if (status >= 500) return 'retry-later';
      if (status >= 400) return 'fix-request';
      return 'ok';
  }
}
```

```php
<?php
function classify(int $status, ?array $error): string
{
    return match ($error['code'] ?? null) {
        'unauthorized' => 'refresh-token',
        'too_many_requests' => 'wait',
        'conflict' => 'check-state',
        default => $status >= 500 ? 'retry-later' : ($status >= 400 ? 'fix-request' : 'ok'),
    };
}
```

**Send only documented fields.** Because unknown request fields are refused, don't send whole records copied from your own system "just in case". Build each request body from the documented fields.

**Treat ids as opaque.** Record ids (`_id`) are 24-character hexadecimal strings. Store them as strings, compare them as strings, and don't read meaning into them. The same goes for the other identifiers you receive, such as a student's `username` or a document's `shortId`.

**Treat links as opaque.** Follow `documentation_url` in an error, and the `url` of a sign-in link, as they are. Don't build them yourself or parse them.

**Treat a missing field as missing.** An exam nobody priced has no `price` field at all, which is not the same as a price of `0`.

## Deprecation and removal

When something is going to be removed or changed in a breaking way:

1. **We announce it in the [changelog](https://hub.main-team.org/api/changelog)** at least **six months** before it changes, with what is affected and what to use instead.
2. **Responses from the affected operations carry `Deprecation` and `Sunset` headers** for that whole period. `Deprecation` marks the operation as deprecated, and `Sunset` gives the date after which it may stop working.
3. **The removal itself only happens in a new major version.** Nothing is removed from `/v1`. When `/v2` exists, `/v1` keeps working. Retiring `/v1` altogether is also a removal, so it follows the same rule: at least six months' notice in the changelog.

No operation is deprecated at the moment. A deprecation is always announced in the [changelog](https://hub.main-team.org/api/changelog) first.

Have your client watch for the headers, so a deprecation reaches someone who can act on it even if nobody reads the changelog that month:

```js
// Wrap your HTTP call once; log every deprecated operation you still use.
function warnIfDeprecated(method, path, res) {
  const deprecation = res.headers.get('deprecation');
  if (!deprecation) return;
  const sunset = res.headers.get('sunset') ?? 'no date given';
  console.warn(`[main-team-api] ${method} ${path} is deprecated (sunset: ${sunset}). See https://hub.main-team.org/api/changelog`);
}
```

```php
<?php
// $headers: response headers with lower-cased names, as collected by the retry helper.
function warnIfDeprecated(string $method, string $path, array $headers): void
{
    if (!isset($headers['deprecation'])) {
        return;
    }
    $sunset = $headers['sunset'] ?? 'no date given';
    error_log("[main-team-api] {$method} {$path} is deprecated (sunset: {$sunset}). See https://hub.main-team.org/api/changelog");
}
```

## Staying informed

- **The [changelog](https://hub.main-team.org/api/changelog)** lists every version, with breaking changes marked. Each version has its own page, such as [1.0.0](https://hub.main-team.org/api/changelog/1-0-0).
- **The `Deprecation` and `Sunset` headers** reach your logs even when nobody reads the changelog.
- **The version badge on these pages** tells you which version you are reading about.
- **Questions** go to **info@main-team.org**. See [Support](https://hub.main-team.org/api/support).
