# Update one of your students and give them access to this organization

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

## Description

Changes the fields you send and leaves the rest as they are: no field is required, and one you leave out is not cleared. It writes the student’s one record, the one `getStudent` returns and every organization shares, so a change made here shows everywhere.

**Access to this organization.** Leave `activatedPlatformsThisSeason` out and this organization’s `slug` is added to it, unless it already holds `common`. The student then shows in `listOrgStudents` and can be sent a link with `createSigninLink`. This is how you give a student access, so unlike the reads it does not need them to have it already, and an empty body, `{}`, does only that. If you do send `activatedPlatformsThisSeason`, the values you send are added to the stored list, and this organization is added only if you name it. The list only ever grows: nothing is removed, a value it already holds is not added twice, and `[]` adds nothing.

**Fields** follow registration’s rules: `birth` is a date that exists, as `DD/MM/YYYY` (`31/02/2008` is refused), `sex` is `m`, `f` or `n`, `country` is an id from `listCountries`, and `grade`, `city` and `school` take an id or a name. A `city` name is looked up in the student’s country, and a `school` name in their country and city: the ones in this body, or else the stored ones. A name or id that matches nothing is refused; nothing is ever created. `firstName`, `lastName`, `birth` and `sex` can be changed but not cleared: an empty value or `null` is refused. `phone` is cleared with `""`. `password` is refused here: use `setStudentPassword`.

**Email.** A new address sets `emailConfirmed` back to `false`, since nobody has confirmed it yet. Sending the address the student already has, in any letter case, changes nothing. An address another student already has is refused.

Sending the same body again leaves the student as the first call did. Answers with the stored record, references as ids; read the student to have them resolved. Only students your account registered can be updated; another account’s student answers 404, like an unknown id.

## Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `organizationId` | path | yes | string | The organization’s `_id`: 24 hexadecimal digits, as `listOrganizations` (`GET /v1/organization`) lists it. |
| `studentId` | path | yes | string | The student’s id (`_id`), as `registerStudent` returned it. |

## Request body

```json
{
  "firstName": "Jane",
  "lastName": "Doe",
  "birth": "14/05/2008",
  "sex": "f",
  "email": "jane.doe@example.com",
  "email2": "<email2>",
  "phone": "+1 555 0100",
  "country": "6650a1b2c3d4e5f6a7b8c9d1",
  "grade": "10",
  "school": "Springfield High School",
  "city": "Springfield",
  "activatedPlatformsThisSeason": [
    "stem"
  ]
}
```

## Response

`200` Success: `message` is "Student updated successfully.".

```json
{
  "success": true,
  "message": "Student updated successfully.",
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 57,
    "totalPages": 3
  },
  "data": {
    "_id": "6650a1b2c3d4e5f6a7b8c9d0",
    "mainId": "6650a1b2c3d4e5f6a7b8c9d0",
    "username": "XXB1045",
    "firstName": "Jane",
    "lastName": "Doe",
    "fullName": "Jane Doe",
    "email": "jane.doe@example.com",
    "emailConfirmed": false,
    "phone": "+1 555 0100",
    "birth": "14/05/2008",
    "sex": "f",
    "country": "6650a1b2c3d4e5f6a7b8c9d1",
    "city": "6650a1b2c3d4e5f6a7b8c9d2",
    "school": "6650a1b2c3d4e5f6a7b8c9d3",
    "grade": "6650a1b2c3d4e5f6a7b8c9d4",
    "supervisor": "6650a1b2c3d4e5f6a7b8c9d5",
    "partner": "6650a1b2c3d4e5f6a7b8c9d6",
    "activatedPlatformsThisSeason": [
      "common"
    ],
    "createdAt": "2026-09-01T09:30:00.000Z",
    "updatedAt": "2026-09-02T14:05:00.000Z"
  }
}
```

## 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) | `studentId` is not a well-formed id. |
| 400 | [`bad_request`](https://hub.main-team.org/api/errors#bad_request) | `country`, `grade`, `city` or `school` matches no record, by id or by name, or a `city` or `school` name was sent for a student with no country (or, for a school, no city) to look it up in. |
| 400 | [`bad_request`](https://hub.main-team.org/api/errors#bad_request) | `email` is `null`: an address can be changed, not removed. Any other value that is not an address is refused by the body’s rules ("email must be an email"). |
| 400 | [`bad_request`](https://hub.main-team.org/api/errors#bad_request) | `firstName`, `lastName`, `birth`, `sex`, `phone` or `activatedPlatformsThisSeason` is `null`, or `firstName` or `lastName` is empty. These can be changed, not removed; clear `phone` with `""`. |
| 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 `student/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 student of yours has this `studentId`. A student registered by another account answers the same. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | `email` is already another student’s address. The answer does not say whose. |
| 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>/student/<studentId>' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "firstName": "Jane",
  "lastName": "Doe",
  "birth": "14/05/2008",
  "sex": "f",
  "email": "jane.doe@example.com",
  "email2": "<email2>",
  "phone": "+1 555 0100",
  "country": "6650a1b2c3d4e5f6a7b8c9d1",
  "grade": "10",
  "school": "Springfield High School",
  "city": "Springfield",
  "activatedPlatformsThisSeason": [
    "stem"
  ]
}
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>/student/<studentId>', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "firstName": "Jane",
    "lastName": "Doe",
    "birth": "14/05/2008",
    "sex": "f",
    "email": "jane.doe@example.com",
    "email2": "<email2>",
    "phone": "+1 555 0100",
    "country": "6650a1b2c3d4e5f6a7b8c9d1",
    "grade": "10",
    "school": "Springfield High School",
    "city": "Springfield",
    "activatedPlatformsThisSeason": [
      "stem"
    ]
  }),
});
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>/student/<studentId>');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'firstName' => 'Jane',
        'lastName' => 'Doe',
        'birth' => '14/05/2008',
        'sex' => 'f',
        'email' => 'jane.doe@example.com',
        'email2' => '<email2>',
        'phone' => '+1 555 0100',
        'country' => '6650a1b2c3d4e5f6a7b8c9d1',
        'grade' => '10',
        'school' => 'Springfield High School',
        'city' => 'Springfield',
        'activatedPlatformsThisSeason' => [
            'stem',
        ],
    ]),
    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']);
```
