Managing employees

Keep your enterprise's members in step with your HR system by uploading a CSV file. Kesk reads each file as soon as the upload finishes, applies it, and writes a JSON report of what it did to /from_kesk/reports/.

Kesk runs nothing on a schedule of its own. Every change starts with a file you upload, so how often your roster syncs is up to you: hourly, nightly or weekly.

If you have not connected yet, start with the setup steps.

How it works

Which folder you upload to decides what the file means:

FolderEffect
/to_kesk/employees/add/
Invite everyone in the file
/to_kesk/employees/remove/
Remove everyone in the file
/to_kesk/employees/
Treat the file as the complete roster: invite who is new, remove who is missing

A file is processed when:

  • an upload closes with a name ending in .csv, or
  • a file is renamed to a name ending in .csv.

The extension is case-sensitive. roster.CSV or roster.txt is stored and never processed. Files under /to_kesk/ outside these three folders are stored and ignored too.

Renaming is a trigger so that a client can upload under a temporary name and rename when the upload is complete. That keeps a half-written file from being read.

Processing is asynchronous. A successful upload means the file was stored, not that it was valid. The outcome is in the .

Uploaded files are not deleted after processing. Uploading a file with the same name overwrites it and processes it again. Deleting a file changes nothing on your roster.

Adding employees

Upload to /to_kesk/employees/add/.

Each row creates a pending invitation for that email address, and Kesk emails it to the person straight away. The person joins the enterprise by accepting it. An invitation expires 30 days after it is sent.

  • The invitation stores the row's first_name, last_name and external_id.
  • It carries the role named on the row, which the person receives on accepting.
  • A row with no role creates an invitation that names no role.
  • If the same email appears twice in one file, only its first row is used.
  • The invitation email is in English and is sent in the name of the account that created the enterprise.

add/ does not compare the file against your roster. A row for someone who already has a pending invitation, or who is already a member, creates a new invitation and sends another email. Uploading the same add/ file twice invites everyone in it twice (backend#905).

Removing employees

Upload to /to_kesk/employees/remove/.

For each email in the file:

  • a pending invitation to that address is revoked, and
  • an active member whose primary email matches is removed from the enterprise. Their Kesk account stays; only the membership ends.

An email that matches no one is skipped without an error. A suspended member is not matched, so a suspended member stays in the enterprise.

remove/ has no safety threshold. Every matching person is removed, and no one is exempt: administrators are removed like anyone else. If a full roster is uploaded to this folder by mistake, everyone in it loses access in one run. Check the destination folder in your HR system's delivery settings before the first scheduled run (backend#906).

Replacing the roster

Upload to /to_kesk/employees/ itself, not a subfolder.

The file is compared with your current roster, which is every active member with a primary email address plus every pending invitation:

PersonResult
In the file, not on the roster
Invited, as in
On the roster, not in the file
Removed, as in , subject to the
In both
Left unchanged

Things this does not do:

  • It does not update anyone. For someone already on the roster, a changed name, role or external_id in the file is ignored. Change a member's roles with updateUser.
  • It protects no one. Anyone active and absent from the file is removed, administrators included. Make sure the people who administer your enterprise are in the file (backend#906).
  • It ignores suspended members. They are not on the roster, so leaving them out removes nothing. Listing one sends them a new invitation.
  • It never removes a member without a primary email address. Kesk has no address to compare with the file.

Only include people who should be able to spend on the enterprise account.

File requirements

RequirementValue
Encoding
UTF-8
Delimiter
Comma
Quoting
RFC 4180: wrap a field containing a comma or a quote in double quotes
Header row
Required. The first row names the columns
Maximum size
50 MB

Header names are case-insensitive and may appear in any order. Columns with other names are ignored, and whitespace around every value is trimmed.

ColumnRequiredAlso accepted asNotes
email
Yes
e-mail, mail
Lowercased. A row whose email is empty or has no @ is skipped and reported
first_name
No
firstname, given_name
last_name
No
lastname, family_name, surname
role
No
One role name. It must exactly match the name of an active role in your enterprise, or that row fails
external_id
No
employee_id, id
Your identifier for the person, stored on the invitation
department
No
dept
Accepted, but currently has no effect (backend#905)

A file without an email column is not processed at all, and produces no report. That includes a file with no header row, whose first data row is read as the header. Nothing changes on your roster, and nothing appears in /from_kesk/reports/ to tell you why (backend#907). If a report does not appear after an upload, check the header row first.

Role names are the names an administrator gave your roles. Users and roles shows how to read them off existing members.

Sample CSV

Rows 3 and 5 leave optional values empty. Rows 4 and 6 show quoting, which is optional unless a value contains a comma or a quote. Enterprise Member and Enterprise Manager are the names of platform-provisioned roles; use the names your enterprise actually has.

Safety threshold

A full-roster upload to /to_kesk/employees/ is checked before anything is applied, to catch a truncated or empty export. Let N be the size of your current roster (active members plus pending invitations) and R the number of people the file would remove.

Roster sizeThe file is rejected when
More than 5
R is at least 2 and R × 100 ÷ N, rounded down, is more than 20
5 or fewer
R is at least 2 and either the file has no valid rows or R is at least half of N

A single removal is never rejected. Some examples:

NRResult
10
2
Applied (20%)
10
3
Rejected (30%)
101
21
Applied (20.8%, rounded down to 20)
100
21
Rejected (21%)
4
1
Applied
4
2
Rejected (half the roster)

A rejected file changes nothing: no one is removed and no one is invited. Kesk writes a rejection_<timestamp>.json report with status rejected_excessive_churn.

The threshold is fixed. It cannot be raised, lowered or switched off per enterprise (backend#906). If a large removal is intended, upload those people to remove/, which is not checked.

The threshold applies only to /to_kesk/employees/. Neither add/ nor remove/ is checked.

Reports

Every processed file produces one JSON report in /from_kesk/reports/:

Upload folderReport name
/to_kesk/employees/
roster_sync_YYYYMMDD_HHMMSS.json
/to_kesk/employees/ (rejected)
rejection_YYYYMMDD_HHMMSS.json
/to_kesk/employees/add/
roster_add_YYYYMMDD_HHMMSS.json
/to_kesk/employees/remove/
roster_remove_YYYYMMDD_HHMMSS.json

The timestamp is the UTC time the report was written, to the second. Reports are read-only: they cannot be deleted over SFTP.

Two files of the same kind that finish processing within the same second get the same report name, and the later report replaces the earlier one. Space out uploads to the same folder (backend#907).

FieldMeaning
file_name
The processed file's internal storage path, sftp/<enterpriseId>/to_kesk/.... Not the path you see over SFTP (backend#907)
file_sha256
SHA-256 of the file as processed. Compare it with your own hash to tie a report to an upload
status
completed, completed_with_errors or rejected_excessive_churn
total_rows
Valid rows in the file. Duplicate emails are counted once, except on remove/
added_count
Invitations created
removed_count
Invitations revoked plus members removed
unchanged_count
Full roster only: people left as they were. On a rejection, the whole roster. 0 for add/ and remove/
error_count
Number of problems
errors
One message per problem

completed_with_errors means the file was applied but at least one row was not. The other rows took effect. Messages look like:

  • Line 4: Empty email address
  • Line 7: Malformed email 'ada.example.com'
  • Failed to invite '[email protected]': One or more of the roles named are not available for this enterprise.
  • Failed to remove member '[email protected]': <reason>

Line numbers count CSV records, with the header as record 1.

An illustrative roster_sync report — the shape is exact, the values are examples:

On a rejection, errors holds the rejection message, and error_count also counts any skipped rows that the message does not list.

Reference

Generated reference — every type, field and argument of the Enterprise GraphQL API, built from the live schema. SFTP itself is not GraphQL and is documented in full in this section.

Built with