Skip to content

Certificates

The Certificates API lets you manage user certificates and qualifications (e.g. safety courses, builder cards, crane licenses) from an external competence or HR system, so site managers in Ditio can see at a glance who is qualified for what — and when a certificate expires. Certificates are matched to users by employee number.

Base URL: api/v4/integration/certificates · Scope: ditioapiv3

Examples use the environment variables from Environments and default to the test environment.

POST /api/v4/integration/certificates

Send an array of certificates. The operation is an upsert: if a certificate already exists for the employee (matched by employee number + certificate type), it is updated; otherwise a new one is created. This makes the endpoint safe to call repeatedly from a scheduled sync.

Entries are processed one at a time, in order. The employee is looked up by employee number across your company structure.

Terminal window
curl -X POST "$DITIO_API_BASE/api/v4/integration/certificates" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '[
{
"employeeNumber": "1042",
"certificateType": "Byggekort",
"certificateNumber": "BK-2026-12345",
"issuedDateTime": "2026-01-15T00:00:00Z",
"validUntilDateTime": "2028-01-15T00:00:00Z",
"notes": "Utstedt av opplæringskontoret"
},
{
"employeeNumber": "1042",
"certificateType": "Kranførerbevis",
"issuedDateTime": "2025-06-01T00:00:00Z",
"validUntilDateTime": "2027-06-01T00:00:00Z"
}
]'
FieldTypeRequiredDescription
employeeNumberstringYesMust match an existing employee in your company structure
certificateTypestringYesType of certificate (e.g. “Byggekort”, “Kranførerbevis”). See Certificate types
certificateNumberstringNoThe number printed on the certificate. Stored on the certificate. On an update, leaving it out keeps the stored number
issuedDateTimedatetimeYesDate the certificate was issued
validUntilDateTimedatetimeYesExpiration date
notesstringNoAdditional notes

certificateNumber is stored on the certificate. When you update an existing certificate, a certificateNumber you send replaces the stored one, and leaving it out keeps the number that is already stored.

  • An entry with a missing employeeNumber, or one that matches no employee, fails.
  • Dates are not validated. Nothing checks that issuedDateTime is before validUntilDateTime, and an omitted date is stored as 0001-01-01. Always send both dates.
  • If a current certificate with the same employeeNumber + certificateType already exists, it is updated rather than duplicated. Only certificateNumber, issuedDateTime, validUntilDateTime and notes are updated, and only when one of them differs from what is stored.

certificateType is matched against the certificate types configured for your company, ignoring case and spaces: "Kranførerbevis", "kranførerbevis" and "Kran førerbevis" all match the same type. The same rule finds the existing certificate to update.

If no configured type matches, Ditio creates a new certificate type with that name on your parent company. A typo therefore creates a new type rather than failing, so keep your certificateType values consistent with the types set up in Ditio Web.

200 with an array of the certificates that were created or changed. Entries that matched an existing certificate with identical values are not included, so an empty array means nothing changed. Items use Ditio’s stored field names rather than the request names, including id, userId (the employee’s identity id), typeId, issued, validTo and notes.

If any entry fails, the whole response is 400, with one item in errors per failed entry. Entries processed successfully in the same request are still saved, but they are not listed in the 400 response. Because the endpoint is an upsert, fixing the failed entries and resending the whole array is safe. See Errors for the error body.

POST /api/v4/integration/certificates/attachments?employeeNumber={employeeNumber}&certificateType={certificateType}

Attach images or PDF scans of the physical card (front and back, for example) to a certificate. You identify the certificate with the same key as the upsert, employeeNumber + certificateType, so you never need a Ditio id. The files are added to the employee’s current certificate of that type, which means the certificate must exist first: create it with Create or update certificates, then upload the files.

Send one or more files as multipart/form-data, each in a part named file.

ParamRequiredEffect
employeeNumberYesThe employee’s number, as in the upsert
certificateTypeYesThe certificate type, as in the upsert. Matching ignores case and spaces, see Certificate types
replaceExistingFilesWithSameNameNoDefault false. When true, a file your company uploaded earlier to this certificate with the same original file name is replaced instead of added again. Use it when you re-sync the same scans

replaceExistingFilesWithSameName only replaces files your company uploaded. Attachments added by anyone else, for example the employee in the mobile app, are never replaced, even when the file name is the same. If one request sends the same file name twice, the last file is kept.

Terminal window
curl -X POST "$DITIO_API_BASE/api/v4/integration/certificates/attachments?employeeNumber=1042&certificateType=Byggekort&replaceExistingFilesWithSameName=true" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/byggekort-front.jpg" \
-F "file=@/path/to/byggekort-back.jpg"

Response (200):

{
"successful": true,
"message": "Files uploaded and attached ok",
"certificateId": "66c1a2b3c4d5e6f7a8b9c0d4",
"files": [
{
"fileReference": {
"id": "66b1a2c3d4e5f6a7b8c9d0e2",
"fileNameOrig": "byggekort-front.jpg"
}
}
],
"attachments": ["66b1a2c3d4e5f6a7b8c9d0e1", "66b1a2c3d4e5f6a7b8c9d0e2"]
}

certificateId is Ditio’s id of the certificate the files were attached to. files has one entry per file attached by this request; fileReference.id is the stored file’s id and fileReference.fileNameOrig the file name you sent. Each entry carries more file metadata than shown here. attachments lists the ids of all files on the certificate after the upload, including earlier ones, without any that were replaced.

StatusWhen
200The files were uploaded and attached
400employeeNumber or certificateType is blank, no employee has that employeeNumber, or the body has no file part
404The employee has no current certificate of that type. Create it first with the upsert above
415The body is not multipart/form-data

See Errors for the error body.

Python example — upload scans after a sync

Section titled “Python example — upload scans after a sync”
import os
import requests
# Paste get_token (/getting-started/authentication/#getting-a-token-in-your-scripts) above this.
API_BASE = os.environ.get("DITIO_API_BASE", "https://core-api.ditio.dev/core")
headers = {"Authorization": f"Bearer {get_token('ditioapiv3')}"}
# 1. The certificate must exist: create or update it first.
requests.post(
f"{API_BASE}/api/v4/integration/certificates",
headers=headers,
json=[{
"employeeNumber": "1042",
"certificateType": "Byggekort",
"certificateNumber": "BK-2026-12345",
"issuedDateTime": "2026-01-15T00:00:00Z",
"validUntilDateTime": "2028-01-15T00:00:00Z",
}],
).raise_for_status()
# 2. Attach the scans to that certificate.
with open("byggekort-front.jpg", "rb") as front, open("byggekort-back.jpg", "rb") as back:
response = requests.post(
f"{API_BASE}/api/v4/integration/certificates/attachments",
headers=headers,
params={
"employeeNumber": "1042",
"certificateType": "Byggekort",
"replaceExistingFilesWithSameName": "true",
},
files=[
("file", ("byggekort-front.jpg", front, "image/jpeg")),
("file", ("byggekort-back.jpg", back, "image/jpeg")),
],
)
response.raise_for_status()
print(response.json()["attachments"])

Python example — sync from a competence system

Section titled “Python example — sync from a competence system”
import os
import requests
# Paste get_token (/getting-started/authentication/#getting-a-token-in-your-scripts) above this.
API_BASE = os.environ.get("DITIO_API_BASE", "https://core-api.ditio.dev/core")
certificates = [
{
"employeeNumber": "1042",
"certificateType": "Byggekort",
"issuedDateTime": "2026-01-15T00:00:00Z",
"validUntilDateTime": "2028-01-15T00:00:00Z",
},
]
response = requests.post(
f"{API_BASE}/api/v4/integration/certificates",
headers={"Authorization": f"Bearer {get_token('ditioapiv3')}"},
json=certificates,
)
response.raise_for_status()
IssueCauseFix
400 for one entryemployeeNumber is missing or doesn’t match a Ditio employeeCreate the user first via Users or Employees v5, then resend
Dates show as 0001-01-01A date field was omittedSend both issuedDateTime and validUntilDateTime on every entry
404 when uploading attachmentsThe employee has no current certificate of that type yetCreate the certificate with the upsert first, then upload the files
Uploaded scans appear twiceThe same file was uploaded again without replaceExistingFilesWithSameName=trueSend replaceExistingFilesWithSameName=true on re-syncs
Unexpected new certificate types, or duplicatesCertificate type spelled differently between syncsKeep certificateType values stable. Matching ignores case and spaces, but any other difference creates a new type and a separate certificate