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

Terminal window
# Test (default for all examples)
export DITIO_API_BASE="https://core-api.ditio.dev/core"
# Production: export DITIO_API_BASE="https://core-api.ditio.app/core"
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: the created user, including identityId and companyProfileId.

{
"identityId": "auth0|abc123def456",
"companyProfileId": "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.

{
"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

Any one of these — they’re tried in order, and the first match wins:

FormExampleNotes
Employee number240Recommended. Validated, and the number you already sync on
Phone number+4798765432 or 98765432Matched against the user’s login number. Local format is normalised using the company’s country code
Profile ID (companyProfileId)001234-698db70bed0b5f4ab12e4367Exact match, restricted to your own company tree
Ditio user ID (identityId)3f2a9c14-8b7d-4e52-9a01-6c4d2e8f1b73Taken as-is, not verified — see the warning below
Full nameOla Nordmann"first last", substring match — see the caution below

Both of a user’s identifiers work, so you can pass whichever one you already hold:

FieldLooks likeNotes
identityId3f2a9c14-8b7d-4e52-9a01-6c4d2e8f1b73Accepted, but not checked against a real user
companyProfileId001234-698db70bed0b5f4ab12e4367Accepted, and validated against your company tree

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

Optional query parameters:

ParameterTypeDescription
changedSincedatetimeOnly return users modified after this date (note: slower query)
companyIdstringFilter to a specific company (useful if you have subsidiaries)
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}
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/auth0%7Cabc123def456" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identityId": "auth0|abc123def456",
"companyProfileId": "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/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 keyidentityId (URL-encode it)companyProfileId
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/67a1b2c3d4e5f6a7b8c9d0e3" \
-H "Authorization: Bearer $TOKEN"
# Enable
curl -X PATCH "$DITIO_API_BASE/api/v4/integration/users/enable/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/auth0%7Cabc123def456" \
-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. OffboardingPATCH /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. On older builds PATCH dropped the field — retry with PUT
Supervisor listed with "name": nullThe supervisorId was a GUID that matches no user. Use the employee number
400 Could not resolve SupervisorId '001234-…'A companyProfileId on a deployment older than profile-ID support, or a profile belonging to another company. Send the employee number
400 Could not resolve SupervisorId '…'The name, employee number, or phone number matched no user in the company
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, …).