# Move one of your students’ applications to another exam

- Endpoint: `PUT /v1/{organizationId}/application/{applicationId}`
- Production: `https://api.main-team.org/v1/{organizationId}/application/{applicationId}`
- Sandbox: `https://apisnd.main-team.org/v1/{organizationId}/application/{applicationId}`
- Operation: `moveApplication` (Applications)
- Authentication: `Authorization: Bearer <token>`, a short-lived token you sign with your API key and secret
- Permission: `application/update:$org:$ID`

## Description

Points an application of one of your students at a different exam. It keeps its `_id` and its payment record. Use it to change the language, the sitting or, where the category allows, the category. The student is not in the body: the application already says whose it is.

The new exam has to be one `listAvailableExams` offers the student, by the same rules and messages as `createApplication`. The application being moved does not count against itself, so changing the language on the same sitting is allowed; every other application the student holds still counts.

**Checks, in order.** The first that fails decides the answer.

1. The application belongs to one of your students (`404`, as for an unknown id).
2. The exam has not been started or handed in (`409`).
3. The new exam exists in this organization (`404`).
4. It is the exam the application already has: `200` "Application already uses that exam." and nothing changes.
5. The new exam is offered to the student (`409`, or `400` for a student with no grade).
6. The old exam’s category accepts the new exam’s category (`409`).
7. The student holds no other exam in that category on that sitting (`409`).
8. An application whose current exam is in the AI Challenge category cannot move once the student has used any of their image quota (`409`).
9. A settled payment (`paid`, with an `amount` above 0) moves only to an exam of the same price (`409`, naming both figures).

**Side effects.** A payment that is not settled is re-priced to the new exam: `paid` with `amount` 0 for a free exam, `pending` at its price otherwise, so a move from a free exam to a priced one leaves the student owing that price. A settled payment keeps its amount and status. A move onto a make-up sitting makes the application expire 6 hours later (`removeAfter`); a move off one clears that.

Safe to retry: sending the exam it already has answers `200` and changes nothing. `data` is the application as stored, with `exam`, `payment` and `user` as ids.

## Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `organizationId` | path | yes | string | The organization’s `_id`: 24 hexadecimal digits, as `listOrganizations` (`GET /v1/organization`) lists it. |
| `applicationId` | path | yes | string | The application’s `_id`, from `createApplication` or one of the application lists. |

## Request body

Required fields: `examId`.

```json
{
  "examId": "6650a1b2c3d4e5f6a7b8c9e1"
}
```

## Response

`200` Success: `message` is "Application updated successfully.". Or `message` is "Application already uses that exam.": The application already has that exam; nothing changed.

```json
{
  "data": {
    "_id": "6650a1b2c3d4e5f6a7b8c9e5",
    "createdAt": "2026-09-01T09:30:00.000Z",
    "exam": "6650a1b2c3d4e5f6a7b8c9e1",
    "examStart": "2026-11-14T10:04:12.000Z",
    "examSubmitted": false,
    "participated": false,
    "partners": [
      {
        "accepted": true,
        "user": "6650a1b2c3d4e5f6a7b8c9d6"
      }
    ],
    "payment": "6650a1b2c3d4e5f6a7b8c9e6",
    "removeAfter": "2026-09-02T14:05:00.000Z",
    "simulationStart": "2026-11-07T10:00:00.000Z",
    "simulationStarted": false,
    "simulationSubmitted": false,
    "submitDate": "2026-11-14T11:12:40.000Z",
    "updatedAt": "2026-09-02T14:05:00.000Z",
    "user": "6650a1b2c3d4e5f6a7b8c9e9",
    "uuid": "a3f-09c-7e1"
  },
  "message": "Application updated successfully.",
  "success": true
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | [`bad_request`](https://hub.main-team.org/api/errors#bad_request) | The body is not valid JSON, breaks a field’s rules, or has a field this operation does not accept ("property <name> should not exist"). |
| 400 | [`bad_request`](https://hub.main-team.org/api/errors#bad_request) | `applicationId` is not 24 hexadecimal digits. |
| 400 | [`bad_request`](https://hub.main-team.org/api/errors#bad_request) | The exam is open, but the student has no grade. Set one with `updateOrgStudent` or `updateStudent`, then try again. |
| 401 | [`unauthorized`](https://hub.main-team.org/api/errors#unauthorized) | The token is missing or malformed, is not signed with your account’s `apiSecret`, breaks the `iat` and `exp` rules, has expired or been revoked, or its account is not active. All of these answer the same. |
| 403 | [`forbidden`](https://hub.main-team.org/api/errors#forbidden) | The token is valid, but no role on your account allows `application/update` on the organization in the path, or a role denies it. |
| 404 | [`not_found`](https://hub.main-team.org/api/errors#not_found) | `organizationId` is not the `_id` of an organization. |
| 404 | [`not_found`](https://hub.main-team.org/api/errors#not_found) | No application of your students has this id: it matches nothing, or the application belongs to another account’s student. Both get the same answer. |
| 404 | [`not_found`](https://hub.main-team.org/api/errors#not_found) | `examId` is not the `_id` of an exam in this organization. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The student has started or handed in the exam. Their answers belong to its questions, so the application stays where it is. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The exam is not open: applications to it are switched off, its sitting date has passed, or its category is inactive. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The exam does not accept the student’s grade. The message names the grades it does accept, such as "Exam is not available for grade 8. It accepts grade 9, 10." |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The exam is restricted to countries the student is not in. The message names the countries it is offered in. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The exam has no language set, so it is offered to no student. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | For any other reason, `listAvailableExams` leaves the exam out for this student. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The category of the application’s current exam does not accept the new exam’s category as a replacement. The message names the current category. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The student already holds another exam in the same category on the same sitting, and nobody can sit both. Move that application (`moveApplication`) instead. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The current exam is in the AI Challenge category and the student has already used some of their image quota. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The payment is settled and the new exam costs something different. This API neither charges a difference nor refunds; the message names both figures. |
| 413 | [`payload_too_large`](https://hub.main-team.org/api/errors#payload_too_large) | The body is larger than 100 kB. |
| 415 | [`unsupported_media_type`](https://hub.main-team.org/api/errors#unsupported_media_type) | The body declares a charset that is not a UTF one (send UTF-8), or a `Content-Encoding` other than gzip, deflate or br. |
| 429 | [`too_many_requests`](https://hub.main-team.org/api/errors#too_many_requests) | Your account has made more than 100 requests to this operation in the current 60-second window. Wait the seconds in `Retry-After` before sending again. |
| 500 | [`internal_error`](https://hub.main-team.org/api/errors#internal_error) | Something failed on our side. Retry later, and quote `request_id` if it goes on. |

## Code samples

### curl

```bash
# $TOKEN: a short-lived token you minted with your API key and secret
curl -sS -X PUT 'https://api.main-team.org/v1/<organizationId>/application/<applicationId>' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "examId": "6650a1b2c3d4e5f6a7b8c9e1"
}
JSON
```

### Node.js

```js
const token = process.env.TOKEN; // a short-lived token you minted with your API key and secret

const res = await fetch('https://api.main-team.org/v1/<organizationId>/application/<applicationId>', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "examId": "6650a1b2c3d4e5f6a7b8c9e1"
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
console.log(body.data);
```

### PHP

```php
<?php
$token = getenv('TOKEN'); // a short-lived token you minted with your API key and secret

$ch = curl_init('https://api.main-team.org/v1/<organizationId>/application/<applicationId>');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'examId' => '6650a1b2c3d4e5f6a7b8c9e1',
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
if ($response === false) {
    throw new RuntimeException(curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$body = json_decode($response, true);
if ($status >= 400) {
    $error = $body['error'];
    throw new RuntimeException("$status {$error['code']}: {$error['message']}");
}
print_r($body['data']);
```
