Skip to content

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.

POST /api/v4/integration/users
Terminal window
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",
"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.

FieldTypeFormatDescription
companyIdstringYour Ditio company ID
employeeNumberstringUnique employee number within the company
firstNamestringFirst name
lastNamestringLast name
mobileWorkstring+4798765432Work phone number (used for SMS login)
birthDatestringdd.MM.yyyy or yyyy-MM-ddDate of birth
employmentStartDatestringdd.MM.yyyy or yyyy-MM-ddEmployment start date
FieldTypeDescription
emailstringEmail address
mobilePrivatestringPrivate phone number
workTitlestringJob title
departmentstringDepartment name
employmentEndDatestringEmployment end date (to terminate employment forward in time)
mainProjectNumberstringDefault project number for this employee
carRegNumberstringCompany car registration number
addressstringHome address
closestRelativestringEmergency contact information
personalInfostringAdditional personal information
cardIdstringBuilder card / access card ID
cardExpirationDatestringCard expiration date
payrollbooltrue = hourly wage, false = fixed salary
isDisabledboolDisable the user account (default: false)
defaultResourceNumberstringDefault resource/machine number
worktimeArrangementNamestringWork time arrangement name
organizationNumberstringOrganization number (for sub-contractor companies)
tagsarrayUser tags to attach or detach (see below)
immediateSupervisorsarrayWho the user reports to (see below)

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 }
]
}
FieldTypeDescription
namestringTag name. Matched case- and whitespace-insensitively
enabledbooltrue 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.

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 }
]
}
FieldTypeDescription
supervisorIdstringIdentifies the supervisor — see the accepted forms below
primaryboolMarks the main supervisor. At most one entry may be true

supervisorId is resolved in this order, and the first step that matches wins:

StepFormExampleNotes
1Ditio user ID (identityId)3f2a9c14-8b7d-4e52-9a01-6c4d2e8f1b73The identityId of a user in your company structure
2Profile ID (companyProfileId)YOUR_COMPANY_ID-67a1b2c3d4e5f6a7b8c9d0e4Exact match, restricted to the user’s company structure
3Employee number, phone number or full name240, +4798765432, Ola NordmannOne 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.)

The list replaces whatever the user has now:

You sendResult
field omitted, or nullsupervisors left untouched
[]all supervisors removed
one or more entriesthose 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.

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.

Terminal window
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 /api/v4/integration/users

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

ParameterTypeDescription
changedSincedatetimeOnly return users modified after this date (note: slower query)
companyIdstringReturn only the users of this company (it must be one your client can manage)
employmentLookbackDaysintOnly return users with active or recently ended employments (within X days)
Terminal window
# All users changed in the last 7 days
curl "$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"
GET /api/v4/integration/users/by-profile-id/{profileId}
GET /api/v4/integration/users/by-employee-number/{employeeNumber}

Looks the number up across your company structure.

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 userGroupId if 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.
ParameterRequiredDescription
projectCompanyIdYesA project company in your company structure
employeeNumberYesThe employee’s number in the main company
userGroupIdYesRole 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.

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.

Terminal window
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"
}'
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.

Terminal window
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.

Both merge, so choose on ergonomics rather than semantics:

PUT /{identityId}PATCH /{companyProfileId}
URL keyidentityIdcompanyProfileId
Required fields9 mandatory fields, else 400none
Fields you omitunchangedunchanged
Explicit nullclears the fieldignored
Best forfull record sync from an HR mastertargeted updates

Instead of deleting a user, disable and re-enable them:

Terminal window
# Disable
curl -X PATCH "$DITIO_API_BASE/api/v4/integration/users/disable/YOUR_COMPANY_ID-67a1b2c3d4e5f6a7b8c9d0e3" \
-H "Authorization: Bearer $TOKEN"
# Enable
curl -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 /api/v4/integration/users/{identityId}
Terminal window
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.

  1. Initial load — create all employees with POST /api/v4/integration/users
  2. Ongoing sync — periodically fetch GET /api/v4/integration/users?changedSince=LAST_SYNC_TIME and compare with your HR system
  3. Updates — use PATCH to update changed fields
  4. Offboarding — PATCH /disable/{companyProfileId} when an employee leaves (or set employmentEndDate)
SymptomCause
GET shows "immediateSupervisors": null after a successful writeExpected — read immediateSupervisorDetails instead
immediateSupervisorDetails is [] after a 200Nothing was stored. Check that the request carried a non-empty immediateSupervisors array
Supervisor listed with "name": nullThe 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 attachedA 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 primaryMore than one entry had primary: true
Supervisor disappeared after an updateThe request sent immediateSupervisors: [], which clears the list. Omit the field to leave it alone

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, …).