> ## Documentation Index
> Fetch the complete documentation index at: https://docs.exode.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Апсерт пользователя

> Создание нового пользователя или обновление существующего в школе с указанием данных профиля

<Tip>
  **Upsert** — это операция "создать или обновить". Если пользователь с указанным email, телефоном, Telegram ID или
  внешним идентификатором `extId` уже существует — его данные будут обновлены. Если пользователя нет — он будет создан.
</Tip>

## Заголовки запроса

<ParamField header="Authorization" type="string" required>
  API токен сервисного пользователя в формате Bearer. Получите токен в панели администратора школы. Формат: `Bearer YOUR_TOKEN`.
</ParamField>

<ParamField header="Seller-Id" type="string" required>
  Уникальный идентификатор продавца в системе. Используется для разграничения доступа между разными продавцами.
</ParamField>

<ParamField header="School-Id" type="string" required>
  Уникальный идентификатор школы в системе. Определяет контекст выполнения операции.
</ParamField>

```
PUT /saas/v2/user/upsert
```

Требуется аутентификация и право `SchoolManageUsers`.

## Параметры запроса

##### Для определения существующего пользователя по логину — передайте хотя бы один из ниже перечисленных параметров, в противном случае пользователь будет создаваться при каждом запросе.

<Info>
  В случае если **пользователь найден** по указанным ниже полям — произойдет только его обновление.
  **В случае создания** — логин и пароль для входа отправятся новому пользователю **автоматически**.
</Info>

<ParamField body="email" type="string" required={false}>
  Email адрес пользователя. Должен быть валидным email форматом. При передаче пустой строки — преобразуется в `null`.
</ParamField>

<ParamField body="phone" type="string" required={false}>
  Номер телефона пользователя. Должен быть в международном формате (например, +9876543210). При передаче пустой строки
  — преобразуется в `null`.
</ParamField>

<ParamField body="tgId" type="integer" required={false}>
  Telegram ID пользователя. Целое число или `null`.
</ParamField>

<ParamField body="extId" type="string" required={false}>
  Внешний идентификатор пользователя из вашей системы. Строка или `null`.
</ParamField>

### Параметры профиля

<ParamField body="profile" type="object" required={false}>
  Объект с данными профиля пользователя.

  <Expandable title="Свойства профиля">
    <ParamField body="firstName" type="string" required={false}>
      Имя пользователя. Максимум 15 символов, только буквы (латиница и кириллица) и пробелы. Автоматически
      обрезаются пробелы в начале и конце.
    </ParamField>

    <ParamField body="lastName" type="string" required={false}>
      Фамилия пользователя. Максимум 15 символов, только буквы (латиница и кириллица) и пробелы. Автоматически
      обрезаются пробелы в начале и конце.
    </ParamField>

    <ParamField body="bdate" type="string" required={false}>
      Дата рождения в формате YYYY-MM-DD.
    </ParamField>

    <ParamField body="sex" type="enum" required={false}>
      Пол пользователя. Возможные значения: `Women`, `Men`.
    </ParamField>

    <ParamField body="role" type="enum" required={false}>
      Роль пользователя. Возможные значения: `Student`, `Tutor`, `Parent`.
    </ParamField>
  </Expandable>
</ParamField>

<Info>
  При создании пользователя автоматически создается связанный профиль с указанными данными. Если профиль не указан,
  создается пустой профиль, поля `firstName` и `lastName` будут заполнены пользователем при первом входе в аккаунт.
</Info>

<RequestExample>
  ```bash cURL theme={null}
  curl --location --request PUT 'https://api.exode.biz/saas/v2/user/upsert' \
    --header 'Seller-Id: {{ sellerId }}' \
    --header 'School-Id: {{ schoolId }}' \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer YOUR_TOKEN' \
    --data-raw '{
      "email": "user@example.com",
      "phone": "+9876543210",
      "extId": "crm_12345",
      "tgId": null,
      "banned": false,
      "profile": {
        "firstName": "Firstname",
        "lastName": "Lastname",
        "bdate": "1990-01-01",
        "sex": "Men",
        "role": "Student"
      }
    }'
  ```

  ```javascript Node.js theme={null}
  const axios = require('axios');

  const upsertUser = async () => {
    try {
      const response = await axios.put('https://api.exode.biz/saas/v2/user/upsert', {
        email: 'user@example.com',
        phone: '+9876543210',
        extId: 'crm_12345',
        tgId: null,
        banned: false,
        profile: {
          firstName: 'Firstname',
          lastName: 'Lastname',
          bdate: '1990-01-01',
          sex: 'Men',
          role: 'Student'
        }
      }, {
        headers: {
          'Seller-Id': '{{ sellerId }}',
          'School-Id': '{{ schoolId }}',
          'Content-Type': 'application/json',
          'Authorization': 'Bearer YOUR_TOKEN'
        }
      });

      console.log('User upserted:', response.data.payload.user);
      console.log('Is created:', response.data.payload.isCreated);
    } catch (error) {
      console.error('Error:', error.response?.data || error.message);
    }
  };

  upsertUser();
  ```

  ```php PHP theme={null}
  <?php

  $url = 'https://api.exode.biz/saas/v2/user/upsert';
  $data = [
    'email' => 'user@example.com',
    'phone' => '+9876543210',
    'extId' => 'crm_12345',
    'tgId' => null,
    'banned' => false,
    'profile' => [
      'firstName' => 'Firstname',
      'lastName' => 'Lastname',
      'bdate' => '1990-01-01',
      'sex' => 'Men',
      'role' => 'Student'
    ]
  ];

  $headers = [
    'Seller-Id: {{ sellerId }}',
    'School-Id: {{ schoolId }}',
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_TOKEN'
  ];

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
  curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
  curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

  $response = curl_exec($ch);
  $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);

  if ($httpCode === 200) {
    $result = json_decode($response, true);
    echo "User upserted successfully\n";
    echo "Is created: " . ($result['payload']['isCreated'] ? 'true' : 'false') . "\n";
    print_r($result['payload']);
  } else {
    echo "Error: HTTP $httpCode\n";
    echo $response;
  }
  ?>
  ```

  ```python Python theme={null}
  import requests
  import json

  url = 'https://api.exode.biz/saas/v2/user/upsert'

  data = {
    'email': 'user@example.com',
    'phone': '+9876543210',
    'extId': 'crm_12345',
    'tgId': None,
    'banned': False,
    'profile': {
      'firstName': 'Firstname',
      'lastName': 'Lastname',
      'bdate': '1990-01-01',
      'sex': 'Men',
      'role': 'Student'
    }
  }

  headers = {
    'Seller-Id': '{{ sellerId }}',
    'School-Id': '{{ schoolId }}',
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_TOKEN'
  }

  try:
    response = requests.put(url, json=data, headers=headers)
    response.raise_for_status()

    result = response.json()
    print('User upserted successfully:')
    print(f"Is created: {result['payload']['isCreated']}")
    print(json.dumps(result['payload'], indent=2, ensure_ascii=False))

  except requests.exceptions.RequestException as e:
    print(f'Error: {e}')
    if hasattr(e, 'response') and e.response is not None:
      print(f'Response: {e.response.text}')
  ```

  ```bsl 1С theme={null}
  Профиль = Новый Структура;
  Профиль.Вставить("firstName", "Firstname");
  Профиль.Вставить("lastName", "Lastname");
  Профиль.Вставить("bdate", "1990-01-01");
  Профиль.Вставить("sex", "Men");
  Профиль.Вставить("role", "Student");

  Данные = Новый Структура;
  Данные.Вставить("email", "user@example.com");
  Данные.Вставить("phone", "+9876543210");
  Данные.Вставить("extId", "crm_12345");
  Данные.Вставить("tgId", Неопределено);
  Данные.Вставить("banned", Ложь);
  Данные.Вставить("profile", Профиль);

  // Serialize body to JSON
  ЗаписьJSON = Новый ЗаписьJSON;
  ЗаписьJSON.УстановитьСтроку();
  ЗаписатьJSON(ЗаписьJSON, Данные);
  ТелоЗапроса = ЗаписьJSON.Закрыть();

  Соединение = Новый HTTPСоединение("api.exode.biz", 443, , , , 30, Новый OpenSSLSecureConnection);

  Запрос = Новый HTTPЗапрос("/saas/v2/user/upsert");
  Запрос.Заголовки.Вставить("Seller-Id", "{{ sellerId }}");
  Запрос.Заголовки.Вставить("School-Id", "{{ schoolId }}");
  Запрос.Заголовки.Вставить("Content-Type", "application/json");
  Запрос.Заголовки.Вставить("Authorization", "Bearer YOUR_TOKEN");
  Запрос.УстановитьТелоИзСтроки(ТелоЗапроса);

  Ответ = Соединение.ВызватьHTTPМетод("PUT", Запрос);

  Если Ответ.КодСостояния = 200 Тогда
      ЧтениеJSON = Новый ЧтениеJSON;
      ЧтениеJSON.УстановитьСтроку(Ответ.ПолучитьТелоКакСтроку());
      Результат = ПрочитатьJSON(ЧтениеJSON);
      Сообщить("Пользователь создан или обновлён");
  Иначе
      Сообщить("Ошибка: HTTP " + Ответ.КодСостояния);
      Сообщить(Ответ.ПолучитьТелоКакСтроку());
  КонецЕсли;
  ```
</RequestExample>

<ResponseExample>
  ```json Success - User Created theme={null}
  {
    "success": true,
    "code": 200,
    "payload": {
      "isCreated": true,
      "user": {
        "id": 1684,
        "createdAt": "2026-07-02T11:15:46.896Z",
        "updatedAt": "2026-07-02T11:15:46.940Z",
        "archivedAt": null,
        "uuid": "e-cjTT0CWMCB",
        "active": true,
        "activated": true,
        "banned": false,
        "alive": true,
        "domain": "id1684",
        "email": "user@example.com",
        "phone": "+9876543210",
        "tgId": null,
        "vkId": null,
        "appleId": null,
        "extId": "crm_12345",
        "schoolId": 198,
        "language": null,
        "timezone": null,
        "lastOnlineAt": null,
        "starsBalance": 0,
        "currentTime": "2026-07-02T11:15:46+00:00",
        "isSleepingNow": false,
        "profile": {
          "id": 1666,
          "createdAt": "2026-07-02T11:15:46.932Z",
          "updatedAt": "2026-07-02T11:15:46.932Z",
          "archivedAt": null,
          "userId": 1684,
          "official": false,
          "firstName": "Firstname",
          "lastName": "Lastname",
          "fullName": "Firstname Lastname",
          "fullNameShort": "Firstname L.",
          "bdate": null,
          "sex": "Ufo",
          "country": null,
          "city": null,
          "role": "Student",
          "status": null,
          "title": "",
          "emojiTitle": "",
          "avatar": {
            "id": 1666,
            "small": "https://storage.exode.biz/production/user/1683/xK2mVwNib9b0/small/avatar.png",
            "medium": "https://storage.exode.biz/production/user/1683/xK2mVwNib9b0/medium/avatar.png",
            "maximum": "https://storage.exode.biz/production/user/1683/xK2mVwNib9b0/avatar.png"
          },
          "titleState": {
            "manualTitle": null,
            "manualEmojiTitle": null,
            "manualNextTitle": null,
            "manualNextEmojiTitle": null,
            "manualExpiredAt": null,
            "locationTitle": null,
            "locationEmojiTitle": null,
            "achievementTitle": null,
            "achievementEmojiTitle": null
          }
        }
      }
    }
  }
  ```

  ```json Success - User Updated theme={null}
  {
    "success": true,
    "code": 200,
    "payload": {
      "isCreated": false,
      "user": {
        "id": 1684,
        "createdAt": "2026-07-02T11:15:46.896Z",
        "updatedAt": "2026-07-02T11:15:46.940Z",
        "archivedAt": null,
        "uuid": "e-cjTT0CWMCB",
        "active": true,
        "activated": true,
        "banned": false,
        "alive": true,
        "domain": "id1684",
        "email": "user@example.com",
        "phone": "+9876543210",
        "tgId": null,
        "vkId": null,
        "appleId": null,
        "extId": "crm_12345",
        "schoolId": 198,
        "language": null,
        "timezone": null,
        "lastOnlineAt": null,
        "starsBalance": 0,
        "currentTime": "2026-07-02T11:15:46+00:00",
        "isSleepingNow": false,
        "profile": {
          "id": 1666,
          "createdAt": "2026-07-02T11:15:46.932Z",
          "updatedAt": "2026-07-02T11:15:46.932Z",
          "archivedAt": null,
          "userId": 1684,
          "official": false,
          "firstName": "UpdatedFirstname",
          "lastName": "UpdatedLastname",
          "fullName": "UpdatedFirstname UpdatedLastname",
          "fullNameShort": "UpdatedFirstname U.",
          "bdate": null,
          "sex": "Ufo",
          "country": null,
          "city": null,
          "role": "Student",
          "status": null,
          "title": "",
          "emojiTitle": "",
          "avatar": {
            "id": 1666,
            "small": "https://storage.exode.biz/production/user/1683/xK2mVwNib9b0/small/avatar.png",
            "medium": "https://storage.exode.biz/production/user/1683/xK2mVwNib9b0/medium/avatar.png",
            "maximum": "https://storage.exode.biz/production/user/1683/xK2mVwNib9b0/avatar.png"
          },
          "titleState": {
            "manualTitle": null,
            "manualEmojiTitle": null,
            "manualNextTitle": null,
            "manualNextEmojiTitle": null,
            "manualExpiredAt": null,
            "locationTitle": null,
            "locationEmojiTitle": null,
            "achievementTitle": null,
            "achievementEmojiTitle": null
          }
        }
      }
    }
  }
  ```

  ```json Error - Email Is Busy theme={null}
  {
    "code": 400,
    "success": false,
    "cause": "EmailIsBusy",
    "message": "Email is busy",
    "error": "Email is busy"
  }
  ```

  ```json Error - Phone Is Busy theme={null}
  {
    "code": 400,
    "success": false,
    "cause": "PhoneIsBusy",
    "message": "Phone is busy",
    "error": "Phone is busy"
  }
  ```

  ```json Error - Telegram ID Is Busy theme={null}
  {
    "code": 400,
    "success": false,
    "cause": "TgIdIsBusy",
    "message": "Telegram ID is busy",
    "error": "Telegram ID is busy"
  }
  ```

  ```json Error - Invalid Email theme={null}
  {
    "code": 400,
    "success": false,
    "cause": "InvalidEmail",
    "message": "Email must be a valid email address",
    "error": "Email must be a valid email address"
  }
  ```

  ```json Error - Invalid Phone theme={null}
  {
    "code": 400,
    "success": false,
    "cause": "InvalidPhone",
    "message": "Phone must be a valid phone number",
    "error": "Phone must be a valid phone number"
  }
  ```

  ```json Error - Invalid First Name theme={null}
  {
    "code": 400,
    "success": false,
    "cause": "InvalidFirstName",
    "message": "First name must match required format",
    "error": "First name must match required format"
  }
  ```
</ResponseExample>

## Требования к правам доступа

<Check>
  Для создания или обновления пользователя требуется **право на управление пользователями школы**.
</Check>

<Warning>
  Сервисный пользователь должен быть аутентифицирован по токену и иметь соответствующие права доступа к указанной
  школе.
</Warning>

<Info>
  При установке статуса `banned: true` все активные сессии пользователя **автоматически завершаются** — это
  реализовано для обеспечения безопасности.
</Info>
