Your First People Integration
Prerequisites
Before we begin, make sure you have the following:
- A tenant name for your Protime environment (e.g.
acmefromhttps://acme.myprotime.eu) - An OAuth2 client_id and client_secret provided by Protime
- A tool for making HTTP requests (e.g. curl, Postman, or your programming language of choice)
- The department ID of a department in your Protime environment (we’ll use
825in this tutorial) - Completed the Your first API call tutorial
Step 1: Authenticate with people and contract scopes
We need read and write permissions for both people and contracts. The connector-protimeapi-people.write scope also covers the historical field endpoints we use in Step 4.
Request
https://authentication.<environmentURL>/tenants/<tenantName>/connect/token
POST /tenants/acme/connect/token HTTP/1.1
Host: authentication.myprotime.eu
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=your-client-id&client_secret=your-client-secret&scope=connector-protimeapi-people.read connector-protimeapi-people.write connector-protimeapi-contracts.read connector-protimeapi-contracts.writeVerify: The response contains an access_token with all four scopes listed.
{
"access_token": "eyJ...Uc",
"expires_in": 1800,
"token_type": "Bearer",
"scope": "connector-protimeapi-people.read connector-protimeapi-people.write connector-protimeapi-contracts.read connector-protimeapi-contracts.write"
}We’ll use this token for all remaining steps.
Step 2: Create a person with your own identifier
Let’s hire John Doe. Instead of creating him and then looking up the Protime ID, we attach our own HR identifier in the same call using the externalReferenceIdentifier property. From that moment on we can address him as Emp123.
Request
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/people
POST /connector/protimeapi/api/v1/people HTTP/1.1
Host: acme.myprotime.eu
Authorization: Bearer eyJ...Uc
Content-Type: application/json{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@acme.com",
"town": "Brussels",
"countryISOCode": "BE",
"badgeNumber": "6767676",
"employeeNumber": "E-00421",
"inServiceDate": "2026-09-01",
"externalReferenceIdentifier": {
"HRMID": "Emp123"
}
}Verify: You should see a 201 Created response. The Location header contains the URL of the new person, and the body is empty.
HTTP/1.1 201 Created
Location: /connector/protimeapi/api/v1/people/8526firstName, lastName and inServiceDate are the only required fields. The externalReferenceIdentifier holds exactly one key-value pair.
Note
HRMID is a custom reference name, so the pair is stored and becomes reusable. Had we used a predefined key (@badge-number or @employee-number), nothing would be stored – those two always resolve from the badgeNumber and employeeNumber fields, and the entry would act purely as a duplicate guard. If another person already carries HRMID Emp123, the request fails with 409 Conflict.
Step 3: Retrieve the person by your identifier
This is the payoff. We never saved the ID 8526 – we can fetch John with the identifier our own system already knows, by naming the collection and reference in the externalReferences query parameter.
Request
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/people/Emp123?externalReferences=(people,HRMID)
GET /connector/protimeapi/api/v1/people/Emp123?externalReferences=(people,HRMID) HTTP/1.1
Host: acme.myprotime.eu
Authorization: Bearer eyJ...UcVerify: The response is John’s record. Note the id – the API resolved Emp123 to person 8526 for us.
{
"id": 8526,
"changeVersion": "0001E7C5000098FA0013",
"lastName": "Doe",
"firstName": "John",
"email": "john.doe@acme.com",
"town": "Brussels",
"countryISOCode": "BE",
"inServiceDate": "2026-09-01",
"employeeNumber": "E-00421",
"badgeNumber": "6767676"
}Note
The person resource itself does not echo an externalReferences object – here the reference only selects which person to return. References are echoed when a person appears as a nested property of another resource, as we’ll see in Step 5.
Because John has a badge number and an employee number, these two calls reach the same record:
GET /connector/protimeapi/api/v1/people/6767676?externalReferences=(people,@badge-number)
GET /connector/protimeapi/api/v1/people/E-00421?externalReferences=(people,@employee-number)Step 4: Assign the person to a department
Organizational assignments are tracked over time, so a department is assigned from a date. This endpoint uses two references at once: the query parameter selects the person, and the request body points at the department.
Request
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/people/Emp123/department-history?externalReferences=(people,HRMID)
POST /connector/protimeapi/api/v1/people/Emp123/department-history?externalReferences=(people,HRMID) HTTP/1.1
Host: acme.myprotime.eu
Authorization: Bearer eyJ...Uc
Content-Type: application/json{
"department": {
"id": 825
},
"from": "2026-09-01"
}Verify: You should see a 201 Created response with an empty body. Unlike Step 2, this endpoint returns no Location header.
The department can also be addressed by a custom reference instead of its ID, provided that reference is already registered. Departments have no predefined references:
{
"department": {
"externalReferences": {
"customDepartmentCode": "PROD-01"
}
},
"from": "2026-09-01"
}Let’s read the history back, asking for the person reference to be enriched:
Request
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/people/Emp123/department-history?externalReferences=(people,HRMID)
{
"value": [
{
"person": {
"id": 8526,
"externalReferences": {
"HRMID": "Emp123"
}
},
"id": 41207,
"changeVersion": "0001E7C5000098FA0021",
"from": "2026-09-01",
"department": {
"id": 825
}
}
]
}until is absent because the assignment is still open. Assigning a later department closes this entry automatically.
The same pattern works for employer-history, job-history, job-category-history, sector-history and work-location-history.
Step 5: Create a contract
A contract records the employment terms. Here the person is identified from the request body, because this endpoint has no route parameter to resolve.
Request
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/contracts
POST /connector/protimeapi/api/v1/contracts HTTP/1.1
Host: acme.myprotime.eu
Authorization: Bearer eyJ...Uc
Content-Type: application/json{
"person": {
"externalReferences": {
"HRMID": "Emp123"
}
},
"code": "CONTRACT-2026",
"from": "2026-09-01",
"contractHoursInMinutes": 2280,
"fullTimeEquivalentInMinutes": 2400,
"numberOfWorkingDaysPerWeek": 5,
"numberOfDaysInContractPeriod": 7,
"kind": "Contract"
}Verify: You should see a 201 Created response with a Location header.
HTTP/1.1 201 Created
Location: /connector/protimeapi/api/v1/contracts/186numberOfDaysInContractPeriod must be a multiple of 7 between 7 and 364. Both minute values must be between 1 and 5999.
Now fetch the contract and ask for the person’s reference to be included:
Request
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/contracts/186?externalReferences=(people,HRMID)
Verify: The nested person object carries our identifier alongside the Protime ID, so the contract can be matched to an employee in our own system without a lookup table.
{
"changeVersion": "0001E7C5000098FA0030",
"id": 186,
"contractPercentage": 95.0,
"person": {
"id": 8526,
"externalReferences": {
"HRMID": "Emp123"
}
},
"code": "CONTRACT-2026",
"from": "2026-09-01",
"contractHoursInMinutes": 2280,
"fullTimeEquivalentInMinutes": 2400,
"numberOfWorkingDaysPerWeek": 5,
"numberOfDaysInContractPeriod": 7,
"kind": "Contract"
}contractPercentage is returned by Protime and is not part of the request body – you cannot set it.
Step 6: Update the person
John moved house. We address him by the same identifier, this time on a PUT.
Request
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/people/Emp123?externalReferences=(people,HRMID)
PUT /connector/protimeapi/api/v1/people/Emp123?externalReferences=(people,HRMID) HTTP/1.1
Host: acme.myprotime.eu
Authorization: Bearer eyJ...Uc
Content-Type: application/json{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@acme.com",
"address": "Main Street 1",
"postalCode": "2800",
"town": "Mechelen",
"countryISOCode": "BE",
"badgeNumber": "6767676",
"employeeNumber": "E-00421",
"inServiceDate": "2026-09-01"
}Verify: You should see a 200 OK response with an empty body.
Caution
PUT replaces the person’s data rather than patching it. firstName, lastName and inServiceDate are required on every call, and any optional field you leave out is sent as empty. Always send the complete object.
Note the two differences from Step 2: the reference moved from the body to the query parameter, and externalReferenceIdentifier is ignored here. To change a reference on an existing person, use the external references endpoints.
Step 7: Track changes with delta
Rather than re-reading every person on each sync, delta returns only what changed. We start one by adding the delta query parameter to the list endpoint.
Request
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/people?delta
GET /connector/protimeapi/api/v1/people?delta HTTP/1.1
Host: acme.myprotime.eu
Authorization: Bearer eyJ...UcUnlike the clockings delta, the people delta takes no filter – this endpoint accepts none.
Verify: The response contains a value array with the current people. If there is a nextLink, we must follow it to page through all data.
{
"value": [
{
"id": 8526,
"changeVersion": "0001E7C5000098FA0042",
"lastName": "Doe",
"firstName": "John",
"town": "Mechelen",
"inServiceDate": "2026-09-01"
}
],
"nextLink": "/connector/protimeapi/api/v1/people?continuationToken=eyJ2YW...&deltaToken=eyJkZW..."
}We follow every nextLink until we reach a page that carries a deltaLink instead. That final page signals we have consumed all initial data.
{
"value": [],
"deltaLink": "/connector/protimeapi/api/v1/delta/people?deltaToken=eyJkZWx0YUlkIjo2NzcsInN0YXJ0aW5nQ3Vyc29yIjowLCJ0aW1lU3RhbXAiOiIyMDI2LTA5LTAxVDEwOjAwOjAwLjAwMDAwMDBaIn0="
}Save that deltaLink – it is our bookmark. The initial pages already reflect John’s updated town, so nothing is outstanding right now. The next time his record changes – say a colleague adds his telephone number – polling the bookmark returns just that:
{
"value": [
{
"changeType": "InsertOrUpdate",
"data": {
"id": 8526,
"changeVersion": "0001E7C5000098FA0055",
"lastName": "Doe",
"firstName": "John",
"telephone": "+32 2 123 45 67",
"town": "Mechelen",
"inServiceDate": "2026-09-01"
}
}
],
"deltaLink": "/connector/protimeapi/api/v1/delta/people?deltaToken=eyJkZWx0YUlkIjo2NzcsInN0YXJ0aW5nQ3Vyc29yIjoxMjM0LCJ0aW1lU3RhbXAiOiIyMDI2LTA5LTAxVDEwOjA1OjAwLjAwMDAwMDBaIn0="
}Each change carries a changeType of InsertOrUpdate or Delete, and a data object with the full record. Each response replaces the previous deltaLink – store the new one for your next poll.
Caution
A delta expires after 72 hours. Call the deltaLink at least once within that window, even when you expect no changes. If it expires, the API returns 410 Gone and you must reinitialize.
What you’ve accomplished
- Authenticated with people and contract scopes
- Created a person and registered your own identifier in the same call
- Retrieved, assigned, contracted and updated that person without ever storing a Protime ID
- Learned the three places a reference can appear: the
externalReferenceIdentifierbody property on create, theexternalReferencesquery parameter on a route, and a nestedexternalReferencesobject pointing at a related resource - Initialized delta tracking and learned how later changes surface as
InsertOrUpdateevents
Next steps
- Real-time sync with webhooks – replace delta polling with push notifications
- How to use external references – registering, listing and deleting custom references
- People reference – full endpoint and property documentation
- Contracts reference – addenda, parent contracts and deletion
Related concepts
- External reference model – why references exist and when to choose predefined over custom
- Delta synchronization – how change tracking works under the hood