> ## 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.

# Состояния пользователя

> REST-эндпоинты для чтения и записи состояния пользователя по ключу

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

<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>

<Info>
  Состояния пользователя — это ключ-значение записи в базе, связанные с конкретным пользователем.
  Чтение и запись доступны только для ограниченного списка ключей (whitelist). Для некоторых ключей применяется
  маскирование при чтении.

  Эндпоинты ниже работают в контексте школы и требуют прав управления пользователями школы.

  Установка значения через PUT <u>полностью замещает предыдущее значение ключа</u>.
  Частичных обновлений (merge) нет.
</Info>

## Список поддерживаемых ключей

<AccordionGroup>
  <Accordion title="UtmSignupParams">
    * Тип: object.

    * GET/SET: да/да.

    * Маскирование: нет.

    * Назначение: UTM/реферальные параметры первичного визита/регистрации.

    * <code>UtmSignupParams</code> хранится как объект произвольных строковых пар ключ-значение (например, utm\_source, utm\_medium, fbclid).

    ```json Success (GET UtmSignupParams) theme={null}
    {
      "success": true,
      "code": 200,
      "payload": {
        "value": {
          "fbclid": "EAA12xQ9YzGpKJ7tMoNybfBcGgGSsSixioGWZRcjvdAUCzvIjyGEXStvsIJC1wKj8ZpJ3kTn5Q2sL0mH7u9vW",
          "utm_term": "9876543210123456789",
          "utm_medium": "paid",
          "utm_source": "google",
          "utm_content": "creative_42_variant_b",
          "utm_campaign": "back_to_school_2025_ru"
        }
      }
    }
    ```

    ```json Body (SET UtmSignupParams) theme={null}
    {
      "value": {
        "utm_source": "google",
        "utm_medium": "paid",
        "utm_campaign": "back_to_school_2025_ru",
        "utm_content": "creative_42_variant_b",
        "utm_term": "9876543210123456789",
        "fbclid": "EAA12xQ9YzGpKJ7tMoNybfBcGgGSsSixioGWZRcjvdAUCzvIjyGEXStvsIJC1wKj8ZpJ3kTn5Q2sL0mH7u9vW"
      }
    }
    ```
  </Accordion>

  <Accordion title="PersonalInfoFilled">
    * Тип: boolean.
    * GET/SET: да/да.
    * Маскирование: нет.
    * Назначение: Флаг заполнения персональных данных.
    * <code>PersonalInfoFilled</code> — булево значение <code>true / false</code>, отражающее факт заполнения персональных данных.

    ```json Success (GET PersonalInfoFilled) theme={null}
    {
      "success": true,
      "code": 200,
      "payload": {
        "value": true
      }
    }
    ```

    ```json Body (SET PersonalInfoFilled) theme={null}
    {
      "value": true
    }
    ```
  </Accordion>

  <Accordion title="OnBoardingProgress">
    * Тип: object.
    * GET/SET: да/да.
    * Маскирование: нет.
    * Назначение: прогресс прохождения онбординга пользователем.

    ```json Body (SET OnBoardingProgress) theme={null}
    {
      "value": { "step": 3, "completed": false }
    }
    ```
  </Accordion>

  <Accordion title="ContentCategoryIds">
    * Тип: array.
    * GET/SET: да/да.
    * Маскирование: нет.
    * Назначение: выбранные пользователем категории контента (массив ID).

    ```json Body (SET ContentCategoryIds) theme={null}
    {
      "value": [12, 45, 78]
    }
    ```
  </Accordion>
</AccordionGroup>

<Info>
  Запись (`SET`) разрешена только для ключей: `UtmSignupParams`, `PersonalInfoFilled`, `OnBoardingProgress`,
  `ContentCategoryIds`. Чтение (`GET`) дополнительно доступно для `VkToken` (возвращается **замаскированным**).
  Запрос к любому другому ключу вернёт ошибку `Not allowed key`.
</Info>

## Получить состояние по ключу

GET /saas/v2/user/:userId/state/get?key=PersonalInfoFilled

### Параметры

<ParamField path=":userId" type="integer" required>
  ID пользователя в рамках школы.
</ParamField>

<ParamField query="key" type="string" required>
  Ключ состояния.
</ParamField>

### Ответ

<ResponseField name="value" type="any" required>
  Значение состояния.
</ResponseField>

<RequestExample>
  ```bash cURL (GET) theme={null}
  curl --location 'https://api.exode.biz/saas/v2/user/123/state/get?key=PersonalInfoFilled' \
    --header 'Seller-Id: {{ sellerId }}' \
    --header 'School-Id: {{ schoolId }}' \
    --header 'Authorization: Bearer YOUR_TOKEN'
  ```

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

  async function getState() {
    const { data } = await axios.get(
      'https://api.exode.biz/saas/v2/user/123/state/get',
      {
        params: { key: 'PersonalInfoFilled' },
        headers: {
          'Seller-Id': '{{ sellerId }}',
          'School-Id': '{{ schoolId }}',
          'Authorization': 'Bearer YOUR_TOKEN',
        },
      }
    );
    console.log(data);
  }

  getState();
  ```

  ```php PHP (GET) theme={null}
  <?php

  $url = 'https://api.exode.biz/saas/v2/user/123/state/get?key=PersonalInfoFilled';
  $headers = [
    'Seller-Id: {{ sellerId }}',
    'School-Id: {{ schoolId }}',
    'Authorization: Bearer YOUR_TOKEN'
  ];

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_HTTPGET, true);
  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);

  echo $response;
  ?>
  ```

  ```python Python (GET) theme={null}
  import requests

  url = 'https://api.exode.biz/saas/v2/user/123/state/get'
  headers = {
    'Seller-Id': '{{ sellerId }}',
    'School-Id': '{{ schoolId }}',
    'Authorization': 'Bearer YOUR_TOKEN'
  }

  response = requests.get(url, params={ 'key': 'PersonalInfoFilled' }, headers=headers)
  print(response.json())
  ```

  ```bsl 1С (GET) theme={null}
  Соединение = Новый HTTPСоединение("api.exode.biz", 443, , , , 30, Новый OpenSSLSecureConnection);

  Запрос = Новый HTTPЗапрос("/saas/v2/user/123/state/get?key=PersonalInfoFilled");
  Запрос.Заголовки.Вставить("Seller-Id", "{{ sellerId }}");
  Запрос.Заголовки.Вставить("School-Id", "{{ schoolId }}");
  Запрос.Заголовки.Вставить("Authorization", "Bearer YOUR_TOKEN");

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

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

  ```bash cURL (SET) theme={null}
  curl --location --request PUT 'https://api.exode.biz/saas/v2/user/123/state/set?key=PersonalInfoFilled' \
    --header 'Seller-Id: {{ sellerId }}' \
    --header 'School-Id: {{ schoolId }}' \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer YOUR_TOKEN' \
    --data-raw '{
      "value": true
    }'
  ```

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

  async function setState() {
    const { data } = await axios.put(
      'https://api.exode.biz/saas/v2/user/123/state/set',
      { value: true },
      {
        params: { key: 'PersonalInfoFilled' },
        headers: {
          'Seller-Id': '{{ sellerId }}',
          'School-Id': '{{ schoolId }}',
          'Content-Type': 'application/json',
          'Authorization': 'Bearer YOUR_TOKEN',
        },
      }
    );
    console.log(data);
  }

  setState();
  ```

  ```php PHP (SET) theme={null}
  <?php

  $url = 'https://api.exode.biz/saas/v2/user/123/state/set?key=PersonalInfoFilled';
  $headers = [
    'Seller-Id: {{ sellerId }}',
    'School-Id: {{ schoolId }}',
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_TOKEN'
  ];

  $payload = json_encode([ 'value' => true ]);

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
  curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
  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);

  echo $response;
  ?>
  ```

  ```python Python (SET) theme={null}
  import requests

  url = 'https://api.exode.biz/saas/v2/user/123/state/set'
  headers = {
    'Seller-Id': '{{ sellerId }}',
    'School-Id': '{{ schoolId }}',
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_TOKEN'
  }

  response = requests.put(url, json={ 'value': { 'step': 3 } }, params={ 'key': 'PersonalInfoFilled' }, headers=headers)
  print(response.json())
  ```

  ```bsl 1С (SET) theme={null}
  Данные = Новый Структура;
  Данные.Вставить("value", Истина);

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

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

  Запрос = Новый HTTPЗапрос("/saas/v2/user/123/state/set?key=PersonalInfoFilled");
  Запрос.Заголовки.Вставить("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 (GET) theme={null}
  {
    "success": true,
    "code": 200,
    "payload": {
      "value": true
    }
  }
  ```

  ```json Error (GET) - Not allowed key theme={null}
  {
    "code": 401,
    "success": false,
    "cause": "Unauthorized",
    "message": "Not allowed key KeyName",
    "error": "Not allowed key KeyName"
  }
  ```

  ```json Success (SET) theme={null}
  {
    "success": true,
    "code": 200,
    "payload": {
      "set": true
    }
  }
  ```

  ```json Error (SET) - Not allowed key theme={null}
  {
    "code": 401,
    "success": false,
    "cause": "Unauthorized",
    "message": "Not allowed key KeyName",
    "error": "Not allowed key KeyName"
  }
  ```
</ResponseExample>

## Установить состояние по ключу

PUT /saas/v2/user/:userId/state/set?key=PersonalInfoFilled

### Параметры

<ParamField path=":userId" type="integer" required>
  ID пользователя в рамках школы.
</ParamField>

<ParamField query="key" type="string" required>
  Ключ состояния.
</ParamField>

<ParamField body="value" type="any" required>
  Значение, записываемое в состояние. Тип зависит от ключа.
</ParamField>

### Ответ

<ResponseField name="set" type="boolean" required>
  Флаг успешной записи состояния.
</ResponseField>

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

<Check>
  Требуется аутентификация по токену и право управления пользователями школы (SchoolManageUsers).
</Check>
