Users (v4)
The Users API lets you create and manage employee accounts in Ditio — the typical backbone of an HR-system sync. Each user needs at minimum a name, phone number, date of birth, employee number, and employment start date.
Base URL: api/v4/integration/users · Scope: ditioapiv3
Every endpoint requires an API client with the Administrator role; other
callers get 403. Looking up, changing or deleting a user your client can’t
manage returns 404. Naming a company you can’t manage (companyId on
create or list, projectCompanyId) returns 403.
Examples use the environment variables from Environments and default to the test environment.
Create a user
Section titled “Create a user”POST /api/v4/integration/userscurl -X POST "$DITIO_API_BASE/api/v4/integration/users" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "companyId": "YOUR_COMPANY_ID", "employeeNumber": "1042", "firstName": "Kari", "lastName": "Nordmann", "mobileWork": "+4798765432", "birthDate": "1990-05-15", "employmentStartDate": "2026-03-01", "email": "[email protected]", "department": "Anlegg", "workTitle": "Maskinfører", "immediateSupervisors": [ { "supervisorId": "240", "primary": true } ], "tags": [ { "name": "safety-course", "enabled": true } ] }'immediateSupervisors attaches the employee’s foreman or site manager, and
tags attaches qualifications — both in the same call that creates the user.
Neither is required; see Supervisors and
User tags for the details.
Response: 200 OK with the created user, including identityId and
companyProfileId.
{ "identityId": "b7e4c2a1-5d3f-4a8e-9c61-2f0d8e7a4b15", "companyProfileId": "YOUR_COMPANY_ID-67a1b2c3d4e5f6a7b8c9d0e3", "companyId": "YOUR_COMPANY_ID", "employeeNumber": "1042", "firstName": "Kari", "lastName": "Nordmann", "mobileWork": "+4798765432", "immediateSupervisors": null, "immediateSupervisorDetails": [ { "userId": "3f2a9c14-8b7d-4e52-9a01-6c4d2e8f1b73", "employeeNumber": "240", "name": "Ola Nordmann", "companyId": "YOUR_COMPANY_ID", "primary": true } ], "tags": null}Note that the supervisor you just set comes back under
immediateSupervisorDetails, while immediateSupervisors and tags read as
null. That’s expected — both are write-only fields. See
Reading supervisors back.
Required fields
Section titled “Required fields”| Field | Type | Format | Description |
|---|---|---|---|
companyId | string | Your Ditio company ID | |
employeeNumber | string | Unique employee number within the company | |
firstName | string | First name | |
lastName | string | Last name | |
mobileWork | string | +4798765432 | Work phone number (used for SMS login) |
birthDate | string | dd.MM.yyyy or yyyy-MM-dd | Date of birth |
employmentStartDate | string | dd.MM.yyyy or yyyy-MM-dd | Employment start date |
Optional fields
Section titled “Optional fields”| Field | Type | Description |
|---|---|---|
email | string | Email address |
mobilePrivate | string | Private phone number |
workTitle | string | Job title |
department | string | Department name |
employmentEndDate | string | Employment end date (to terminate employment forward in time) |
mainProjectNumber | string | Default project number for this employee |
carRegNumber | string | Company car registration number |
address | string | Home address |
closestRelative | string | Emergency contact information |
personalInfo | string | Additional personal information |
cardId | string | Builder card / access card ID |
cardExpirationDate | string | Card expiration date |
payroll | bool | true = hourly wage, false = fixed salary |
isDisabled | bool | Disable the user account (default: false) |
defaultResourceNumber | string | Default resource/machine number |
worktimeArrangementName | string | Work time arrangement name |
organizationNumber | string | Organization number (for sub-contractor companies) |
tags | array | User tags to attach or detach (see below) |
immediateSupervisors | array | Who the user reports to (see below) |
User tags
Section titled “User tags”Each entry names a tag and says whether the user should have it. A tag that
doesn’t exist in the company yet is created the first time you use it — but only
when enabled is true. An entry with enabled: false naming a tag the company
doesn’t have is ignored, and creates nothing.
{ "tags": [ { "name": "safety-course", "enabled": true }, { "name": "crane-license", "enabled": false } ]}| Field | Type | Description |
|---|---|---|
name | string | Tag name. Matched case- and whitespace-insensitively |
enabled | bool | true attaches the tag to the user, false detaches it |
Only the tags you list are touched — anything you leave out keeps its current
state. Omitting tags, or sending an empty array, changes nothing.
Supervisors
Section titled “Supervisors”immediateSupervisors sets who a user reports to. This is the field to use when
you create an employee and want their foreman or site manager attached at the
same time.
{ "immediateSupervisors": [ { "supervisorId": "240", "primary": true } ]}| Field | Type | Description |
|---|---|---|
supervisorId | string | Identifies the supervisor — see the accepted forms below |
primary | bool | Marks the main supervisor. At most one entry may be true |
What supervisorId accepts
Section titled “What supervisorId accepts”supervisorId is resolved in this order, and the first step that matches
wins:
| Step | Form | Example | Notes |
|---|---|---|---|
| 1 | Ditio user ID (identityId) | 3f2a9c14-8b7d-4e52-9a01-6c4d2e8f1b73 | The identityId of a user in your company structure |
| 2 | Profile ID (companyProfileId) | YOUR_COMPANY_ID-67a1b2c3d4e5f6a7b8c9d0e4 | Exact match, restricted to the user’s company structure |
| 3 | Employee number, phone number or full name | 240, +4798765432, Ola Nordmann | One search across your company structure that skips disabled users. Employee numbers match exactly; a phone number is matched against the login number after normalising local formats with your company’s country code; a name is a substring of "first last" — see the caution below |
Employee number is the recommended form: it’s an exact match on the
number you already sync on. An employee number, phone number, name or
profile ID that matches no one returns 400:
{ "error": { "message": "Could not resolve SupervisorId 'xyz' to a user in company 'YOUR_COMPANY_ID'" } }(Excerpt; the full body follows the shape on Errors.)
Replace, keep, or clear
Section titled “Replace, keep, or clear”The list replaces whatever the user has now:
| You send | Result |
|---|---|
field omitted, or null | supervisors left untouched |
[] | all supervisors removed |
| one or more entries | those become the complete set |
Two entries resolving to the same person collapse into one. More than one
primary: true returns 400 Only one supervisor can be marked as primary.
Reading supervisors back
Section titled “Reading supervisors back”You write immediateSupervisors but read immediateSupervisorDetails.
GET always returns "immediateSupervisors": null — that’s expected, and not a
sign the write failed. The stored supervisors come back resolved:
{ "immediateSupervisors": null, "immediateSupervisorDetails": [ { "userId": "3f2a9c14-8b7d-4e52-9a01-6c4d2e8f1b73", "employeeNumber": "240", "name": "Ola Nordmann", "companyId": "YOUR_COMPANY_ID", "primary": true } ]}Check immediateSupervisorDetails to confirm a write landed. An empty array
means nothing was stored.
Create a user with a supervisor
Section titled “Create a user with a supervisor”curl -X POST "$DITIO_API_BASE/api/v4/integration/users" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "companyId": "YOUR_COMPANY_ID", "employeeNumber": "1042", "firstName": "Kari", "lastName": "Nordmann", "mobileWork": "+4798765432", "birthDate": "1990-05-15", "employmentStartDate": "2026-03-01", "immediateSupervisors": [ { "supervisorId": "240", "primary": true } ] }'Get users
Section titled “Get users”All users in your company
Section titled “All users in your company”GET /api/v4/integration/usersWithout companyId, a token for the main company of a company structure
returns the users of the main company and its subsidiaries; a token for any
other company returns that company’s users only.
Optional query parameters:
| Parameter | Type | Description |
|---|---|---|
changedSince | datetime | Only return users modified after this date (note: slower query) |
companyId | string | Return only the users of this company (it must be one your client can manage) |
employmentLookbackDays | int | Only return users with active or recently ended employments (within X days) |
# All users changed in the last 7 dayscurl "$DITIO_API_BASE/api/v4/integration/users?changedSince=2026-06-27T00:00:00Z" \ -H "Authorization: Bearer $TOKEN"
# Only active employees (employment still active or ended within 30 days)curl "$DITIO_API_BASE/api/v4/integration/users?employmentLookbackDays=30" \ -H "Authorization: Bearer $TOKEN"By company profile ID
Section titled “By company profile ID”GET /api/v4/integration/users/by-profile-id/{profileId}By employee number
Section titled “By employee number”GET /api/v4/integration/users/by-employee-number/{employeeNumber}Looks the number up across your company structure.
Get or create in a project company
Section titled “Get or create in a project company”GET /api/v4/integration/users/get-or-create-in-project-company?projectCompanyId={id}&employeeNumber={number}&userGroupId={role}Makes sure an employee of the main company has a profile in one of its
project companies, and returns that profile. Despite the GET, this call can
write:
- If the project company already has a profile with this employee number, it
is returned. Its role is changed to
userGroupIdif it differs. - Otherwise, if the main company has an active, enabled employee with this number, a profile is created in the project company with an employment that starts on the first day of the current month, and returned.
| Parameter | Required | Description |
|---|---|---|
projectCompanyId | Yes | A project company in your company structure |
employeeNumber | Yes | The employee’s number in the main company |
userGroupId | Yes | Role in the project company: user, superuser or administrator |
Returns 400 when a parameter is missing or userGroupId is not one of the
three values, 403 when projectCompanyId is outside your company
structure, and 404 when it isn’t a project company or the main company has
no active, enabled employee with that number.
Update a user (full)
Section titled “Update a user (full)”PUT /api/v4/integration/users/{identityId}Despite the name, PUT does not replace the user. It merges the fields you send onto the stored user, exactly like PATCH — a field you leave out keeps its current value rather than being blanked.
What actually separates PUT from PATCH is validation, not merge behaviour: PUT
rejects the request with 400 unless the body carries identityId,
companyProfileId, companyId, employeeNumber, firstName, lastName,
mobileWork, birthDate, and employmentStartDate.
Sending null for a field does clear it on PUT. If you mean “no change”,
leave the field out instead.
curl -X PUT "$DITIO_API_BASE/api/v4/integration/users/b7e4c2a1-5d3f-4a8e-9c61-2f0d8e7a4b15" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "identityId": "b7e4c2a1-5d3f-4a8e-9c61-2f0d8e7a4b15", "companyProfileId": "YOUR_COMPANY_ID-67a1b2c3d4e5f6a7b8c9d0e3", "companyId": "YOUR_COMPANY_ID", "employeeNumber": "1042", "firstName": "Kari", "lastName": "Nordmann", "mobileWork": "+4798765432", "birthDate": "1990-05-15", "employmentStartDate": "2026-03-01", "workTitle": "Anleggsleder" }'Update a user (partial)
Section titled “Update a user (partial)”PATCH /api/v4/integration/users/{companyProfileId}Updates only the fields you include, and requires no mandatory fields. This is
the endpoint to use for routine updates. Note that PATCH uses
companyProfileId in the URL, not identityId.
curl -X PATCH "$DITIO_API_BASE/api/v4/integration/users/YOUR_COMPANY_ID-67a1b2c3d4e5f6a7b8c9d0e3" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "workTitle": "Prosjektleder", "department": "Administrasjon", "immediateSupervisors": [{ "supervisorId": "240", "primary": true }] }'Unlike PUT, an explicit null is ignored by PATCH rather than clearing the
field. To clear a value use PUT — or, for supervisors, send an empty array.
PUT or PATCH?
Section titled “PUT or PATCH?”Both merge, so choose on ergonomics rather than semantics:
PUT /{identityId} | PATCH /{companyProfileId} | |
|---|---|---|
| URL key | identityId | companyProfileId |
| Required fields | 9 mandatory fields, else 400 | none |
| Fields you omit | unchanged | unchanged |
Explicit null | clears the field | ignored |
| Best for | full record sync from an HR master | targeted updates |
Disable / enable a user
Section titled “Disable / enable a user”Instead of deleting a user, disable and re-enable them:
# Disablecurl -X PATCH "$DITIO_API_BASE/api/v4/integration/users/disable/YOUR_COMPANY_ID-67a1b2c3d4e5f6a7b8c9d0e3" \ -H "Authorization: Bearer $TOKEN"
# Enablecurl -X PATCH "$DITIO_API_BASE/api/v4/integration/users/enable/YOUR_COMPANY_ID-67a1b2c3d4e5f6a7b8c9d0e3" \ -H "Authorization: Bearer $TOKEN"Both return 204 No Content on success.
Delete a user
Section titled “Delete a user”DELETE /api/v4/integration/users/{identityId}curl "$DITIO_API_BASE/api/v4/integration/users/is-identity-deletable/b7e4c2a1-5d3f-4a8e-9c61-2f0d8e7a4b15" \ -H "Authorization: Bearer $TOKEN"The response is an object, not a bare boolean. When the user can’t be deleted,
validationErrors explains why:
{ "isDeletable": true, "validationErrors": []}DELETE returns 204 No Content on success.
Common integration patterns
Section titled “Common integration patterns”Sync employees from an HR system
Section titled “Sync employees from an HR system”- Initial load — create all employees with
POST /api/v4/integration/users - Ongoing sync — periodically fetch
GET /api/v4/integration/users?changedSince=LAST_SYNC_TIMEand compare with your HR system - Updates — use
PATCHto update changed fields - Offboarding —
PATCH /disable/{companyProfileId}when an employee leaves (or setemploymentEndDate)
Troubleshooting supervisors
Section titled “Troubleshooting supervisors”| Symptom | Cause |
|---|---|
GET shows "immediateSupervisors": null after a successful write | Expected — read immediateSupervisorDetails instead |
immediateSupervisorDetails is [] after a 200 | Nothing was stored. Check that the request carried a non-empty immediateSupervisors array |
Supervisor listed with "name": null | The supervisorId doesn’t identify a user in your company structure. Use the employee number |
400 Could not resolve SupervisorId '…' | The value matched no enabled user in your company structure, or a profile ID belongs to a company outside the user’s structure. Send the employee number |
| Wrong person attached | A name matched more than one user. Names are substring-matched and ties are broken arbitrarily — use the employee number |
400 Only one supervisor can be marked as primary | More than one entry had primary: true |
| Supervisor disappeared after an update | The request sent immediateSupervisors: [], which clears the list. Omit the field to leave it alone |
Handling employee numbers
Section titled “Handling employee numbers”employeeNumber is the primary identifier for matching users between
systems. It must be unique within a company. If your HR system doesn’t have
employee numbers, generate sequential ones (e.g. 001, 002, …).
Related
Section titled “Related”- Employees API v5 — the modern replacement, recommended for new integrations
- SCIM Provisioning — directory-driven provisioning from Entra ID
- Certificates — attach qualifications to users by employee number
- Help centre: Brukerhåndtering — user administration in Ditio Web (Norwegian)