Skip to main content
Input: the JSON export from the HR system (the client’s structure, as is):
Each item is processed as follows: PUT /saas/v2/user/ext/{extId}/update (updates the profile by external ID); if the employee does not exist (NotFound), POST /saas/v2/user/create (creates the user and hires them via the extra.staff.employments array). The user, department and position are addressed directly by extId, so you do not need our system’s internal IDs at all. extra is a container for related entities: staff.employments for now, and later custom field values and so on.

What actually goes to the API for Ivanov: a 1-to-1 mapping

The input item from the export:
What goes to the API (a new employee: create + hire in a single call):
Response: { "success": true, "code": 201, "payload": { "user": { ... } } }: the user is created and hired into “ПОДР-001” as “Manager” in a single call. An existing employee: update the profile by extId, no internal ID needed (if the user does not exist, you get NotFound, and then you go to create):
No extra.staff here, on purpose: sending the employment again is harmless (an idempotent no-op), but if the employee was transferred to another department in 1C, extra would create a second employment for them (secondary employment) rather than a transfer. HR changes go through /staff/employment/*. extra is also supported in update, in the same format as in create. Let’s look at what happens to each employments element:
The rule is simple: extra.employments can only “make sure the employment exists” (if not, hire; if it does, skip). It CANNOT close or change existing employments:
  • the employee was transferred → POST /staff/employment/ext/{extId}/transfer;
  • promoted (position changed) → POST /staff/employment/ext/{extId}/promote;
  • dismissed → POST /staff/employment/ext/{extId}/terminate.
The mirror consequence: if you do NOT include an existing employment in employments, NOTHING happens to it, and it stays active. The array is not a “full list to sync”: an employment missing from it ≠ termination of that employment. You can remove an employment only with an explicit terminate call. So you can send one employment or all of them: listed ones are hired or skipped, unlisted ones are left untouched. If you send a new pair in extra during a transfer, you get an employee with TWO active employments (the old and the new one), not a transfer. Secondary employment: several employments in one array (up to 10):
The variant without email: sign-in with domain + password (domain is built from login by replacing disallowed characters with _):

What to do when something changes in 1C

The loop above is idempotent: you can run it as often as every hour, and repeated runs are safe. It picks up profile changes on its own, but HR changes it does not; they have separate calls. All of them are addressed by the employment extId (the same extId you passed in the employments element when hiring, which is why it matters to set it). Examples of HR calls (the employment extId from the example above is i.ivanov.01011990:ПОДР-001). For readability, the extId in paths is shown as is; in a real request, encode it (КодироватьСтроку(..., СпособКодированияСтроки.КодировкаURL)), otherwise Cyrillic characters in the path will not arrive correctly: i.ivanov.01011990%3A%D0%9F%D0%9E%D0%94%D0%A0-001.
A nuance with extId on transfer/promote: the employment is closed and a new one is opened, but the extId moves to the new record: after the transfer, the same i.ivanov.01011990:ПОДР-001 still points to the employee’s current employment. If the extId embeds a department code (as in the example), it will no longer match the actual department; this is not an error, it is just a key. If you want precision, use the ID of the employment record from 1C as the extId.

Prerequisites

  • Departments (ПОДР-001, etc.) must be created in advance: POST /saas/v2/staff/department/create { "name": "Sales Department", "extId": "ПОДР-001", "parentExtId": "ПОДР-000" }. If departmentExtId is not found, create returns StaffDepartmentNotFound and the user is not created.
  • HR changes for an existing employee go through the employment endpoints by employment extId: transfer: POST /staff/employment/ext/{extId}/transfer { "toDepartmentExtId": "ПОДР-002" }, position change: .../promote { "toPositionExtId": "<GUID>" }, termination: .../terminate. A new employment (including secondary employment): POST /staff/employment/hire, or extra.staff.employments in user/update/user/upsert, which hires idempotently (repeating the same active department + position pair is a no-op); transfers and terminations cannot be expressed through extra.
  • Termination of the last active employment automatically closes the account: user.status becomes Terminated, all sessions are ended, and sign-in is blocked. A repeated hire (hire) automatically restores Active. You cannot terminate the school owner or yourself (StaffCannotTerminateSchoolOwner / StaffCannotTerminateSelf).
  • The Blocked status (blocked by an admin) is set via user/update { "status": "Blocked" }: sign-in is disabled, sessions are ended, and new course assignments are prohibited; the data is kept and visible in reports. { "status": "Active" } lifts the block, and access is restored. The school owner cannot be blocked.
  • Absences control the OnLeave status: when an employee’s absence becomes effective (vacation, sick leave, etc.), user.status automatically becomes OnLeave; when it ends or is deleted, it returns to Active. OnLeave is an informational status: sign-in remains available.
  • leader_external_ids maps not to the employee but to the department manager, with a separate call: POST /saas/v2/staff/department-manager/set { "departmentExtId": "ПОДР-001", "employmentExtId": "<employment extId>", "isPrimary": true }. Pass isPrimary explicitly on every call: set without it turns the primary manager into a regular one.
  • status from the export maps to user.status: “Работает” (Working) → "Active". “Уволен” (Dismissed) is not passed directly as a status: termination is done via employment/terminate, and Terminated is set automatically. OnLeave is not set manually either; absences control it.
  • role/tag_list/city/email_for_notifications have no counterpart in the API; these fields are ignored.
  • Employees without email: pass domain (Latin letters/digits/_/dots; a dot not at the edges and not consecutive; the server converts it to lowercase; unique within the school; if taken → DomainIsBusy) and always password: without email and phone, domain + password becomes the only way to sign in.
  • Positions by GUID (positionExtId), not by name: the name is mutable and not unique on the 1C side (“someone adds a soft sign and the integration breaks”). The GUID is optional: without it, the name fallback works. Limitation: a position name is unique within the school, so you cannot create two positions with the same name but different GUIDs (different organizations in 1C) (StaffPositionNameIsNotUniq); consolidate the GUIDs into one or do not pass a GUID for the second organization.

Updated: 2026-09-25 14:33 UTC