Skip to main content
Этот гайд описывает типовой сценарий выгрузки сотрудников и оргструктуры из внешней системы (1С:ЗУП, CRM, другая LMS) в Exode: порядок вызовов, маппинг типовых полей и куда передавать данные, для которых нет прямого поля в API.
Ключевой принцип: все сущности связываются через внешние идентификаторы (extId) — GUID или коды из вашей системы. Вам не нужно хранить внутренние ID Exode: департаменты, должности и трудоустройства адресуются по extId, пользователи находятся по extId через user/find и user/upsert.

1С → Exode: готовый пример синхронизации

Полный разобранный пример под 1С:ЗУП: BSL-код цикла выгрузки (update по extId → NotFound → create), JSON-маппинг полей 1-в-1, домены из логинов, должности по GUID и предусловия.

Схема связей

Как связаны сущности модуля staff между собой:
  • Департамент — подразделение школы; иерархия строится через parentId (parentExtId).
  • Трудоустройство — центральная связка: пользователь + департамент + должность, плюс условия занятости (kind, type, rate).
  • Руководитель департамента привязывается не к пользователю напрямую, а к его активному трудоустройству.
  • Отсутствие (отпуск, больничный, командировка) тоже привязано к трудоустройству.

Порядок синхронизации

1

Департаменты

Создайте оргструктуру методами раздела Департаменты. Передавайте extId (код подразделения из вашей системы) и parentExtId для иерархии.Родительский департамент должен существовать на момент создания дочернего — синхронизируйте дерево сверху вниз (или отсортируйте выгрузку так, чтобы родители шли раньше детей).
Департаменты и должности должны быть заведены до синхронизации сотрудников. Если при создании сотрудника departmentExtId не найден — вернётся StaffDepartmentNotFound и пользователь создан не будет (аналогично StaffPositionNotFound).
2

Должности

Создайте должности методами раздела Должностиname и extId. Правильный ключ — GUID должности из вашей системы (extId), а не имя: имя изменяемо и не уникально на вашей стороне (переименовали должность — и резолв по имени сломается). Актуализируйте имя при каждой синхронизации через PUT /staff/position/ext/{extId}/update, а при отсутствии — создавайте (StaffPositionNotFoundPOST /staff/position/create).
Имя должности уникально в рамках школы: две должности с одинаковым именем, но разными extId (например, из разных организаций в 1С) создать нельзя — вернётся StaffPositionNameIsNotUniq. Сведите GUID’ы к одному либо не передавайте GUID у дублирующей организации.
Если у должностей вообще нет кодов (в выгрузке только job_position_name) — работает fallback: найдите должность по имени через GET /staff/position/list?search=..., при отсутствии создайте.
3

Сотрудники

Создать нового сотрудника можно методом create или upsert (существующих обновляют через update). Что выбрать, зависит от вашей выгрузки — в первую очередь от того, нужно ли задавать пароль: подробно в разделе Каким методом заводить сотрудника. Трудоустройства во всех случаях передаются массивом в extra.staff.employments (каждый элемент — positionExtId + departmentExtId, опционально extId, type, kind, startAt, rate). Для корпоративной школы при создании пользователя массив обязателен и должен содержать хотя бы одно трудоустройство (иначе StaffEmploymentInputRequired).Если сотрудник числится в нескольких подразделениях — передайте несколько элементов в extra.staff.employments (от 1 до 10): первое трудоустройство с kind: "Main", остальные — kind: "InternalSecondary" (или ExternalSecondary для внешнего совместительства).
extra.staff.employments умеет только «убедиться, что назначение есть» (нет — нанять, есть — пропустить). Это не полный список для синхронизации: см. раздел Поведение назначений employments ниже — важные нюансы про переводы, увольнения и отсутствующие в массиве назначения.
4

Руководители

Назначьте руководителей департаментов методом staff/department-manager/set: departmentExtId + employmentExtId (внешний идентификатор трудоустройства руководителя, который вы передали при найме). Основной руководитель помечается isPrimary: true.Если в вашей системе руководитель указывается у каждого сотрудника (например, leader_external_ids), а не у подразделения — назначайте руководителем департамента и/или сохраните ссылку на руководителя в кастомном поле сотрудника.
5

Остальные данные — кастомные поля

Для данных, которым нет поля в схеме пользователя (отчество, город, теги, произвольный статус из CRM, дополнительный email), создайте кастомные поля в макете формы и записывайте значения через custom-field/value/set-by-slug после апсерта пользователя.
6

Увольнения

При увольнении вызовите staff/employment/terminate (по extId трудоустройства). Закрытие последнего активного трудоустройства автоматически переводит пользователя в статус Terminated и закрывает ему доступ — отдельно блокировать его не нужно. Если у сотрудника остаются другие активные трудоустройства, увольнение из одного из них — это структурное изменение, доступ сохраняется. Чтобы заблокировать доступ без увольнения, передайте status: "Blocked" в user/update — все активные сессии будут завершены; см. Жизненный цикл статуса.Если ваша система отдаёт полный список сотрудников (а не изменения) — получите текущие активные трудоустройства через staff/employment/list, сравните с выгрузкой по extId и увольте отсутствующих.

Каким методом заводить сотрудника

Создать нового сотрудника умеют только create и upsert. update пользователя не создаёт — он лишь обновляет существующего (и участвует в паттерне «update → NotFound → create»). Выбор между create и upsert зависит от того, нужно ли вам задавать пароль.
  • POST /user/create — создаёт нового. Если пользователь с таким логином (email/phone/domain), tgId или extId уже существует — вернётся ошибка (EmailIsBusy, PhoneIsBusy, DomainIsBusy, TgIdIsBusy, ExtIdIsBusy). Пароль (password) применяется.
  • PUT /user/upsert — «создать или обновить» за один вызов. Ищет существующего по логину / tgId / extId: найден → обновляет, нет → создаёт. Идемпотентен — повторная выгрузка того же сотрудника не создаёт дубль и не падает.
  • PUT /user/ext/{extId}/update (или PUT /user/{userId}/update) — только обновляет существующего. Если пользователя нет — NotFound (создания не будет). Пароль этим методом не задаётся.
Пароль применяется только при создании — в create и в ветке создания upsert. При обновлении (в том числе когда upsert нашёл существующего пользователя) password игнорируется: метод update его не поддерживает.
Как выбрать:
  • Пароль вы не задаёте (генерируете и рассылаете на нашей стороне или он не нужен) → берите upsert: один вызов, идемпотентно, проще всего для регулярной синхронизации.
  • Пароль задаёте вы (например, детерминированный «имя+фамилия+год») → заводите новых через create (там пароль точно применится), а существующих обновляйте через update. Типовой паттерн: update по extId → при NotFoundcreate (всё адресуется по extId, внутренние ID Exode не нужны). Через upsert так не выйдет: если пользователь уже есть, upsert уйдёт в обновление и заданный вами пароль не применится.

Поведение назначений employments

Массив employments в user/create, user/update и user/upsert применяется идемпотентно и умеет ровно одно — «донанять, если такого активного назначения ещё нет». Это ключевое отличие от «полной синхронизации», и его легко упустить:
  • Повтор той же пары департамент + должность — безопасный no-op. Слать одно и то же назначение при каждом синке можно — дубль не создастся.
  • Новая пара — это новое назначение (совместительство), а не перевод. Если сотрудника в 1С перевели и вы пришлёте в employments новую пару, у него станет два активных назначения (старое и новое), а не одно.
  • Отсутствие назначения в массиве ≠ увольнение. Не пришли назначение — с ним ничего не произойдёт, оно останется активным. Убрать его можно только явным terminate.
Поэтому кадровые изменения существующего сотрудника выражаются не через extra, а отдельными эндпоинтами раздела Трудоустройства:
Практический вывод для инкрементальной выгрузки: в user/ext/{extId}/update для существующего сотрудника extra.staff обычно не передают — иначе перевод в 1С превратится в лишнее совместительство. Донайм через extra уместен, когда вы осознанно добавляете сотруднику ещё одно место работы.

Жизненный цикл статуса пользователя

Поле user.status (Active / OnLeave / Banned / Blocked / Terminated / Deleted) частично управляется автоматически. Доступ закрывают Banned, Blocked, Terminated и Deleted; Active и OnLeave — оставляют вход открытым.
  • Увольнение с последнего активного трудоустройства (employment/terminate) автоматически переводит пользователя в Terminated: вход запрещён, все сессии завершаются. Повторный наём (hire) сотрудника со статусом Terminated автоматически возвращает Active. Нельзя уволить с последнего назначения самого себя (StaffCannotTerminateSelf) или владельца школы (StaffCannotTerminateSchoolOwner).
  • Блокировка админомuser/update { "status": "Blocked" }: вход закрыт, сессии завершаются, новые зачисления на продукты запрещены; данные сохраняются и видны в отчётах. { "status": "Active" } снимает блокировку. Владельца школы заблокировать нельзя.
  • OnLeave управляется модулем отсутствий: при действующем отсутствии (отпуск/больничный) статус автоматически становится OnLeave, по завершении — возвращается в Active. OnLeave информационный — вход остаётся доступен, вручную его ставить не нужно.
  • Бан и разбанuser/update { "status": "Banned" } закрывает доступ. При снятии бана (передайте Active) фактический статус пересчитывается автоматически по трудоустройствам и отсутствиям: уволенный вернётся в Terminated, находящийся в отпуске — в OnLeave, остальные — в Active.
Синхронизация OnLeave работает и по календарю: помимо пересчёта при API-вызовах, платформа раз в час автоматически переводит сотрудников в OnLeave при наступлении даты начала отсутствия и возвращает в Active по его окончании. Заводить собственный периодический пересчёт больше не нужно.
Маппинг статуса из выгрузки: «Работает» → Active (снимет ранее выставленную блокировку). «Уволен» не передаётся статусом — увольнение оформляется через employment/terminate, Terminated выставится сам.

Маппинг типовых полей

Тело запроса user/create или user/upsert с комментариями — откуда какое поле берётся и что важно знать (// ← показывает исходное поле вашей выгрузки):
Поля, которым нет прямого соответствия в API:
  • Отчество, город, теги, произвольный статус из CRM, доп. emailкастомные поля (custom-field/value/set-by-slug после создания/апсерта пользователя).
  • Руководители (leader_external_ids) → отдельный вызов staff/department-manager/set (departmentExtId + employmentExtId).
  • Роль/теги без аналога — игнорируются.

Ограничения, о которых стоит знать заранее

  • domain — до 65 символов, латинские буквы, цифры и _; точки допускаются как разделители сегментов (не в начале/конце и не подряд). Логины вида i.ivanov.01011990 передавать можно. Нельзя домен из одних цифр или вида id123. Сервер приводит к нижнему регистру; значение уникально в рамках школы — занятое вернёт DomainIsBusy.
  • Сотрудник без email и телефона входит только по domain — для него обязательно передайте password, иначе войти будет нечем.
  • firstName и lastName — до 15 символов, буквы (латиница/кириллица), пробелы и апострофы. Длинные ФИО обрезайте на своей стороне.
  • extId любой сущности (пользователь, департамент, должность, трудоустройство, отсутствие, руководитель) — до 50 символов, без / и пробелов (используется в путях ext/{extId}); точки допустимы. user.extId — не логин.
  • По extId и tgId нельзя авторизоваться — поля для входа: email, phone, domain.
  • Модуль трудоустройств (staff) доступен только для корпоративных школ; для них массив extra.staff.employments (от 1 до 10 трудоустройств) обязателен при создании пользователя.

Минимальный пример: сотрудник с трудоустройством

cURL
После апсерта запишите нестандартные атрибуты в кастомные поля:
cURL

Обновлено: 2026-07-14 22:58 UTC