Introduction
4HSE provides tools for managing personnel records. However, it’s becoming increasingly common for well-established companies to want to continue using their HR software and integrate it with a security management system like 4HSE. This challenge arises both during the project onboarding phase and in subsequent daily operational phases where maintaining a solid alignment between the two systems is crucial.
We will explore how to develop software that, leveraging the APIs provided by 4hse, allows any HR software to integrate with 4HSE, keeping personnel records data consistently synchronized.
We won’t focus on a specific HR software but aim for a generic approach to the problem by developing an integration script that can be easily adapted to various HR software solutions in the market.
Prerequisites
What do we need to develop an HR integration script?
- Data source: We will use a simple CSV file as a data source, which we imagine to be exported periodically from an HR software database to align with 4hse every 24 hours.
project_id
: We need to have an active project in 4hse and know its ID. You can find the ID in the “info” section of your project window.access_token
: We need an active user with admin role for the mentioned project, as this user will be the one executing the synchronization. To authenticate this user via API, we need its access token. The token is provided by 4hse upon client request.
Data source
To prepare the CSV file extracted from the data source, we must always consider these simple rules:
- The file follows a column layout predefined by 4hse.
- The file always contains the entire dataset of employees, updated.
- The field values for each individual employee in the dataset will overwrite the values for the same employee in 4hse.
- Employees present in 4hse but not in the source will cause the historical tracking of those present in 4hse.
- Employees present in the source but not in 4hse will be added to 4hse.
The CSV file consists of the following columns:
field | required |
---|---|
code | no |
first_name | yes |
last_name | yes |
street | no |
locality | no |
postal_code | no |
region | no |
sex | no |
country | no |
birth_date | no |
birth_place | no |
tax_code | no |
note | no |
is_employee | yes |
Among these fields, it is necessary to identify a key field, i.e., a field whose value uniquely identifies an employee. It can be any of these fields, usually the code
or the tax code tax_code
.
4HSE REST API
4hse provides a standard OpenAPI REST API for executing and monitoring synchronization. The create
endpoint receives a request containing an input CSV and creates a task at the 4hse processing servers. The service responds immediately upon successful task creation, providing the unique task identifier. The task processing will be executed as soon as possible. The view
endpoint provides the processing status of the task identified by its ID.
Complete documentation of the APIs can be found here: PeopleSync API.
Example
In this example, we want to establish periodic synchronization between the data extracted from our HR and the “My Company” project in 4hse. Suppose the CSV file employees-1.csv
has been extracted from our HR software and appropriately transformed to follow the CSV column structure provided by 4hse. The file contains 7 employees.
code | first_name | last_name | street | locality | postal_code | region | sex | country | birth_date | birth_place | tax_code | note | is_employee |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | John | Doe | 123 Main St | Anytown | 12345 | CA | M | USA | 1980-05-15 | Anytown | ABC123456 | Note for John Doe | 1 |
2 | Jane | Smith | 456 Oak St | Someville | 67890 | NY | F | USA | 1985-08-21 | Someville | DEF789012 | Note for Jane Smith | 1 |
3 | Robert | Johnson | 789 Pine St | Another City | 54321 | TX | M | USA | 1972-11-03 | Another City | GHI345678 | Note for Robert Johnson | 1 |
4 | Emily | Davis | 101 Elm St | Yourtown | 98765 | FL | F | USA | 1992-04-18 | Yourtown | JKL901234 | Note for Emily Davis | 1 |
5 | Michael | Miller | 202 Cedar St | Cityville | 13579 | IL | M | USA | 1988-07-27 | Cityville | MNO567890 | Note for Michael Miller | 1 |
6 | Sophia | Brown | 303 Maple St | Smalltown | 24680 | GA | F | USA | 1995-02-09 | Smalltown | PQR123456 | Note for Sophia Brown | 1 |
7 | Daniel | Wilson | 404 Birch St | Newtown | 86420 | WA | M | USA | 1983-09-12 | Newtown | STU789012 | Note for Daniel Wilson | 1 |
ACCESS_TOKEN
: It is the token that identifies the 4hse user who will perform the operation, an admin of the project.PROJECT_ID
: It is the ID of the project we have just created.PRIMARY_KEY
: It is the column in the CSV that uniquely identifies an employee. We have chosen “tax_code.”EMULATION
: It allows us to execute the request in emulation mode. This means we can perform the sync and obtain the results without actually making the changes. This allows us to test the outcome of the operation before applying it. We will initially set this value toTRUE
.MAX_CHANGES
: It is a security parameter that controls the maximum number of changes applicable in the request. If the total number of changes made exceeds this value, we will be alerted, and the request will be rejected. We initially set it to150
.
Now let’s define the createTask
method, which will be used to create the synchronization task at the people-sync
service of 4hse.
The creation of the task is a simple HTTP POST
request to the server, which responds with a task_id
.
The task ID is an identifier for the task that allows us to query the server to know its progress (queued, executed, etc.).
The method only requires the input of the file name to synchronize and provides the output of the created task ID.
private static String createTask(String filePath) throws IOException {
...
}
The first thing to do is to instantiate an HTTP client for sending the request:
private static String createTask(String filePath) throws IOException {
HttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("http://localhost:8081/people-sync/create");
}
Since we need to send a file, the data must be transmitted in multipart format.
Let’s populate the body of the multipart request by providing the file and the remaining input parameters:
private static String createTask(String filePath) throws IOException {
HttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("http://localhost:8081/people-sync/create");
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
// Add file parameter
builder.addBinaryBody("file", new File(filePath),
ContentType.APPLICATION_OCTET_STREAM, "file.csv");
// Add other parameters
builder.addTextBody("access-token", ACCESS_TOKEN);
builder.addTextBody("project_id", PROJECT_ID);
builder.addTextBody("pk", PRIMARY_KEY);
builder.addTextBody("emulation", String.valueOf(EMULATION));
builder.addTextBody("max_changes", String.valueOf(MAX_CHANGES));
HttpEntity multipart = builder.build();
httpPost.setEntity(multipart);
}
Finally, we execute the request.
In case of success, the server will respond with a JSON that needs to be parsed to obtain the task_id
, which we will return as output.
In case of failure, we print the error on the screen.
private static String createTask(String filePath) throws IOException {
HttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("http://localhost:8081/people-sync/create");
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
// Add file parameter
builder.addBinaryBody("file", new File(filePath),
ContentType.APPLICATION_OCTET_STREAM,"file.csv");
// Add other parameters
builder.addTextBody("access-token", ACCESS_TOKEN);
builder.addTextBody("project_id", PROJECT_ID);
builder.addTextBody("pk", PRIMARY_KEY);
builder.addTextBody("emulation", String.valueOf(EMULATION));
builder.addTextBody("max_changes", String.valueOf(MAX_CHANGES));
HttpEntity multipart = builder.build();
httpPost.setEntity(multipart);
// Execute the request
HttpResponse response = httpClient.execute(httpPost);
// Parse the response JSON
JSONParser parser = new JSONParser();
try (InputStream content = response.getEntity().getContent()) {
JSONObject jsonResponse = (JSONObject) parser.parse(new InputStreamReader(content));
return (String) jsonResponse.get("task_id");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
Now, let’s define a method readTask
that, using the task_id
, will obtain the current status of the task and print it on the screen.
private static void readTask(String taskId) throws IOException {
...
}
Similarly to the previous method, we instantiate an HTTP client that, this time, makes a request using the GET method
, providing the task_id and the
access-token` for user identification.
private static void readTask(String taskId) throws IOException {
HttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://localhost:8081/people-sync/view?id=" + taskId + "&access-token=" + ACCESS_TOKEN);
}
We execute the request, parse the JSON response, and print the task_id
on the screen.
private static void readTask(String taskId) throws IOException {
HttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://localhost:8081/people-sync/view?id=" + taskId + "&access-token=" + ACCESS_TOKEN);
// Execute the request
HttpResponse response = httpClient.execute(httpGet);
// Parse and print the response JSON
JSONParser parser = new JSONParser();
try (InputStream content = response.getEntity().getContent()) {
JSONObject jsonResponse = (JSONObject) parser.parse(new InputStreamReader(content));
System.out.println("Task: " + jsonResponse.toJSONString());
} catch (Exception e) {
e.printStackTrace();
}
}
Finally, we define the main method, which will be the entry point of our script. The available commands for the script will be:
Ecco la conversione in Markdown:
-
create
to create the taskjava -jar your-jar-file.jar create filename.csv
-
read
to read the taskjava -jar your-jar-file.jar read task-id
We then check that the input parameters are provided; otherwise, we display a message on the screen.
public static void main(String[] args) {
String command = args[0]; //create or read
if (args.length < 2) {
System.out.println("Usage:");
System.out.println("java -jar your-jar-file.jar create file.csv");
System.out.println("java -jar your-jar-file.jar read task-id");
return;
}
}
We then check if the first parameter passed to the command is create
or read
and call the respective methods, passing the CSV file name as an argument for creation or the task ID for reading.
public static void main(String[] args) {
String command = args[0]; //create or read
if (args.length < 2) {
System.out.println("Usage:");
System.out.println("java -jar your-jar-file.jar create file.csv");
System.out.println("java -jar your-jar-file.jar read task-id");
return;
}
try {
if ("create".equals(args[0])) {
String taskId = createTask(args[1]);
System.out.println("Created Task ID: " + taskId);
}
else if ("read".equals(args[0])) {
readTask(args[1]);
}
else {
System.out.println("Action must be 'create' or 'read'");
}
} catch (IOException e) {
e.printStackTrace();
}
}
Now let’s run the script by initiating the task creation with the command:
java -jar target/my-java-project-1.0-SNAPSHOT-jar-with-dependencies.jar
create employees2-1.csv
In response, we obtain the ID of the created task:
Created Task ID: be9eec92-621c-31b1-a525-7df391f599bf
After waiting for a few seconds, we execute the command to read the status of the created task:
java -jar target/my-java-project-1.0-SNAPSHOT-jar-with-dependencies.jar
read be9eec92-621c-31b1-a525-7df391f599bf
In response, we get the complete report of the server’s actions:
task_id
: task IDentity_id
: project IDuser_id
: the user who created the taskstatus
: indicates whether the task was executed successfully or notcreated_at
,updated_at
: provide temporal references to the creation and update of the taskresponse.result
: is a history of operations performed for each record in our CSV file. In this case, as they were new employees, each employee, identified by theirtax_code
, was added to the project.response.changes
: is a summary of all operations performed. In this case, the task made a total of 7 changes, all of which were “additions.”log
: the server log recorded during the task execution. It is useful for identifying problems in case of errors.
{
"task_id": "a386a5bf-5bc4-3be7-9392-c81c69918bfc",
"entity_id": "41ec7395-eeda-4c8e-bc73-5585d48e5161",
"user_id": "john@4hse.com",
"status": "DONE",
"created_at": "2024-01-11 08:14:53",
"updated_at": "2024-01-11 08:15:07",
"response": {
"result": {
"ABC123456": [
{
"action": "add",
"id": "ABC123456"
}
],
"PQR123456": [
{
"action": "add",
"id": "PQR123456"
}
],
"DEF789012": [
{
"action": "add",
"id": "DEF789012"
}
],
"STU789012": [
{
"action": "add",
"id": "STU789012"
}
],
"MNO567890": [
{
"action": "add",
"id": "MNO567890"
}
],
"GHI345678": [
{
"action": "add",
"id": "GHI345678"
}
],
"JKL901234": [
{
"action": "add",
"id": "JKL901234"
}
]
},
"log": [
{
"level": "INFO",
"text": "Task a386a5bf-5bc4-3be7-9392-c81c69918bfc executed",
"timestamp": 1704960907
},
{
"level": "INFO",
"text": "Executing task a386a5bf-5bc4-3be7-9392-c81c69918bfc in emulation mode",
"timestamp": 1704960906
}
],
"changes": {
"add": 7,
"total": 7,
"activate": 0,
"update": 0,
"deactivate": 0
}
}
}
At this point, let’s change the emulation parameter to false
and rerun the create command.
private final static boolean EMULATION = false;
We will receive the same response as before, but in this case, the changes have been actually executed, and therefore, we will have 7 employees in the “La mia azienda” project.
Now, let’s assume that in the company, 2 people have retired. Consequently, our HR has created a new file employees2-2.csv
containing only the remaining 5 individuals. Additionally, for “John Doe” the NOTE
field has been modified. In total, there are 3 differences compared to the employees2-1.csv
file.
code | first_name | last_name | street | locality | postal_code | region | sex | country | birth_date | birth_place | tax_code | note | is_employee |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | John | Doe | 123 Main St | Anytown | 12345 | CA | M | USA | 1980-05-15 | Anytown | ABC123456 | Note for John Doe | 1 |
2 | Jane | Smith | 456 Oak St | Someville | 67890 | NY | F | USA | 1985-08-21 | Someville | DEF789012 | Note for Jane Smith | 1 |
3 | Robert | Johnson | 789 Pine St | Another City | 54321 | TX | M | USA | 1972-11-03 | Another City | GHI345678 | Note for Robert Johnson | 1 |
4 | Emily | Davis | 101 Elm St | Yourtown | 98765 | FL | F | USA | 1992-04-18 | Yourtown | JKL901234 | Note for Emily Davis | 1 |
5 | Michael | Miller | 202 Cedar St | Cityville | 13579 | IL | M | USA | 1988-07-27 | Cityville | MNO567890 | Note for Michael Miller | 1 |
Let’s modify our script, specifically the MAX_CHANGES
parameter, reducing it to 2.
private final static int MAX_CHANGES = 2;
Let’s run the create command again, this time sending the employees2-2.csv
file:
java -jar target/my-java-project-1.0-SNAPSHOT-jar-with-dependencies.jar create
employees2-2.csv
Read the status of the newly created task, and we encounter an error as the actual number of changes (3) exceeds the maximum allowed by this request (2).
{
"task_id": "b9819d5e-a0a7-34e0-844d-cde54ed91858",
"entity_id": "41ec7395-eeda-4c8e-bc73-5585d48e5161",
"user_id": "john@4hse.com",
"status": "FAILED",
"created_at": "2024-01-11 09:19:03",
"updated_at": "2024-01-11 09:19:21",
"response": {
"log": [
{
"level": "ERROR",
"text": "Out of max changes",
"timestamp": 1704964761
}
]
}
}
Let’s modify the value of MAX_CHANGES
again, setting it to 150
.
private final static int MAX_CHANGES = 150;
Let’s rerun the task creation command and check its status:
{
"task_id": "b7ede071-e5c2-37c9-8075-9cd1b5580f26",
"entity_id": "41ec7395-eeda-4c8e-bc73-5585d48e5161",
"user_id": "john@4hse.com",
"created_at": "2024-01-11 08:22:36",
"updated_at": "2024-01-11 08:23:16",
"status": "DONE",
"response": {
"result": {
"ABC123456": [
{
"changes": {
"note": "Another note for John Doe"
},
"action": "update",
"id": "ABC123456"
}
],
"PQR123456": [
{
"action": "deactivate",
"periods": [
{
"end_date": "2024-01-11 00:00:00",
"start_date": "1970-01-01 00:00:00"
}
],
"id": "PQR123456"
}
],
"STU789012": [
{
"action": "deactivate",
"periods": [
{
"end_date": "2024-01-11 00:00:00",
"start_date": "1970-01-01 00:00:00"
}
],
"id": "STU789012"
}
]
},
"log": [
{
"level": "INFO",
"text": "Task b7ede071-e5c2-37c9-8075-9cd1b5580f26 executed",
"timestamp": 1704961396
},
{
"level": "INFO",
"text": "Executing task b7ede071-e5c2-37c9-8075-9cd1b5580f26 in emulation mode",
"timestamp": 1704961396
}
],
"changes": {
"add": 0,
"total": 3,
"activate": 0,
"update": 1,
"deactivate": 2
}
}
}
The response shows the three expected changes. The two individuals who retired have been historically recorded, and the notes for the relevant employee have been modified.
Conclusions
We have seen how easy it is to synchronize your HR database with 4hse, keeping both systems perfectly aligned. In the next tutorial, we will further extend this example script to leverage more in-depth synchronization mechanisms.