Timeclock Integration - Asure API

Good morning,
I recently got the API documentation from Asure, our timeclock software. There is a GET EMPLOYEE PUNCHES code that I would like to use to pull data into Epicor’s labor tables. I am not clear on how to use the API in Epicor. Where would I put the API authentication, and GET commands in Epicor?
Thanks!
Nate

I built a Salesforce integration to sync data from Kinetic to Salesforce in a function library. I put all the REST Authentication and request functionality in a single function.

This function is called from various BPM’s that call it with 4 parameters provide all the information that is needed to create the desired request:

  • Payload - the json body for the request
    Action - the request method (POST, PATCH or DELETE). In my case, the function doesn’t do GET.
    SFID - the unique identifier for the object record to be patched or deleted.
    SFObject - the Salesforce Object to be affected.

The Authenticate widget uses RestSharp to get an access token.from Salesforce, then that token is used in the requests to patch, post or delete.


var client = new RestClient(URL);
client.Timeout = -1;
var request = new RestRequest(Method.POST);
  request.AddHeader("Cookie", "BrowserId=tYalcW9qEe20mXd58go2fA; CookieConsentPolicy=0:0; LSKey-c$CookieConsentPolicy=0:0");
  request.AddHeader("Content-Type", "application/x-www-form-urlencoded");

  request.AddHeader("Referer", Referrer);
  request.AddHeader("Host", Host);
  request.AddParameter("grant_type", "client_credentials");
  request.AddParameter("client_id", "REDACTED");
  request.AddParameter("client_secret", "REDACTED");
      
IRestResponse response = client.Execute(request);
      string msg = response.Content.ToString();
      Ice.Diagnostics.Log.WriteEntry("Response Message " + msg);

    JObject o = JObject.Parse(msg);
    access_token = (string)o["access_token"];
    signature = (string)o["signature"];
    scope = (string)o["scope"];
    instance_url = (string)o["instance_url"];
    token_type = (string)o["token_type"];
    issued_at = (string)o["issued_at"];
    
    Ice.Diagnostics.Log.WriteEntry("Access " + access_token +" "+ signature +" "+ scope +" "+ instance_url +" "+ token_type +" "+ issued_at);

        AccessToken = access_token;
        TokenType = token_type;
        InstanceURL = instance_url;
        
    Ice.Diagnostics.Log.WriteEntry("Authentication Complete " +" "+ BPMPayload + " " + BPMAction + " " + BPMSFID);
3 Likes

Nice job, @danvoss

In order to make this more secure, people might consider voting for an Epicor Idea that keeps secrets out of our code.

Implement Secrets Management | Epicor Kinetic Ideas Portal (aha.io)

5 Likes

I agree, wish we could do that.

I am mocking this up in Postman. And I am no pro with postman. I barely understand how it works. My goal is to make one or more API calls to our timeclock software. I think there are two important calls to make.

Call 1. GET EMPLOYEES. This returns a list of all the employees with the ID that I have in Epicor, along with the key used by the API to reference an employee. In Postman, I process this with a test script that looks like this:

// Parse the response body as JSON
var jsonData = pm.response.json();

// Define an object to store EmployeeNumber and Key pairs
var employeeData = {};

// Iterate through the JSON data and extract EmployeeNumber and Key
jsonData.forEach(function(employee) {
    employeeData[employee.EmployeeNumber] = employee.Key;
});

// Log the extracted data
console.log(employeeData);

This gets me a list of employee IDs and Keys. I would like to store this for future reference, so Chat GPT told me to add this at the end:

// Set employeeData as a collection variable
pm.collectionVariables.set('employeeData', JSON.stringify(employeeData));

Call 2. GET EMPLOYEE PUNCHES. This returns a list of the punch data for a given employee key and date. I need to refernce the key when making this call, so GPT gave me this pre-request script to use when making this call:

// Parse the stored employeeData collection variable
var employeeData = JSON.parse(pm.collectionVariables.get('employeeData'));

// Define the employee number you want to look up
var employeeNumber = '1234'; // Example employee number

// Look up the employee key using the employee number
var employeeKey = employeeData[employeeNumber];

// Set the employee key as an environment variable for use in the request
pm.environment.set('employeeKey', employeeKey);

This call is formatted as: employees/{employeekey}/punch/{punchdate}

I have a few questions. When I make the call in postman, how do I pass in the employee number, and have that populate the employeekey in the call? How do I make both calls sequentially in Postman so that my employeedata is available for the function to lookup a key? Once I have this all figured out in Postman, can I use all the same code in Epicor? Or do I have to do all the manipulations of the JSON in Epicor? Thanks for your time on this!

After I got postman returning the correct data, I found the “Code” part that generates various code snippets based on your call. This is the rest sharp snippet from my code (redacted).

var options = new RestClientOptions("API CALL LINE HERE")
{
  MaxTimeout = -1,
};
var client = new RestClient(options);
var request = new RestRequest("", Method.Get);
request.AddHeader("Authorization", "Basic AUTHCODEHERE==");
RestResponse response = await client.ExecuteAsync(request);
Console.WriteLine(response.Content);

I think this means that postman can generate at least some of the code I will need to use in Epicor.

In Postman, when I create a “Test” for my JSON response, I get this code:

pm.test("Find the employeenumber and send the key for this employeenumber to the console", function () {
    var employeeNumber = "1234";
    var employeeKey = "";
    pm.response.json().forEach(function(employee) {
        if (employee.EmployeeNumber === employeeNumber) {
            employeeKey = employee.Key;
            console.log("Employee Key: " + employeeKey);
        }
    });
    pm.expect(employeeKey).to.not.equal("");
});

I expect that I need to perform this same action in Epicor. So, I will make my GET API call, and then with the JSON data that is sent back, I have to perform this “Test” to pull out the Key I need to feed the GET PUNCHES call. I assume I will need a BPM and custom code to implement this, as @danvoss posted. How can I convert from this “test” syntax to something epicor will understand?

I’m assuming that you probably want to pull this information daily - probably for the previous day. In that case, you’ll want to build one or more functions to do this. You can schedule a function as a process to run on a designated schedule.

If I were building this, I would add a UD field to EmpBasic to store the employeeKey for that employee. Inserting the employeeKey value would probably be a manual process when you set up the employee.

I would build several functions -

AsureIntegrationMaster (AIM) - Controlling function that is scheduled as a daily process

AsureAuth - A function similar to above that handles the REST authentication and communication. Use RestSharp in this function.

AsureJson - Converts the received Json punch information into a dataset.

AsureLabor - Uses the dataset data to add the labor records.

The flow would be AssureIntegrationMaster is called and loops through EmpBasic where employeeKey != “” and EmpStatus = “A”.

For each employee found, AIM will create the body for the GET command and will pass the body and other required data for the GET to AsureAuth.

AsureAuth gets the Json data and passes it to Asure.Json.

Asure.Json converts the Json to a dataset and is then passes it to Asure.Labor. To work with the Json data, you’ll want to use Newtonsoft.Json.

Asure.Labor loops through each record of punch data and uses it to create the labor records in Kinetic. I’m imagining that this function may need to use multiple BO’s to gather required data for adding a labor record.

When Asure.Labor finished, control returns to AIM and processing of the next employee starts.

1 Like

Is this just to avoid the extra call to link the emp id to the key?
Thank you for this overview. I will be trying to follow along in the coming days.

I think it’s more efficient than having to pull the list of employees from Asure each time and then have to query the list to find the key to get the employee punches.

Either method can work.

1 Like

Why and how do I use Newtonsoft in Epicor? We are cloud DT so I can’t install software like this, right?

Epicor has already added that library since they use it too.

And yes, @klincecum, you can use System.Text.Json too.

1 Like

I just saw it in my assemblies. Thanks!

This line in my first function:

var client = new RestClient(URL);
client.Timeout = -1;
var request = new RestRequest("/webapi/employees/", Method.GET);
request.AddHeader("Authorization", "Basic SOMERANDOMLETTERSHERE");
IRestResponse response = client.Execute(request);
Console.WriteLine(response.Content);

This is some tweaked code that postman generated from my get employees call.

  1. Did postman take my username and password, and somehow convert it into a Basic Auth string (where I have SOMERANDOMLETTERSHERE)?
  2. Once I have the authorization from this function, how do I keep it and use it for the other API calls? My code just puts the response into the console, which doesn’t seem helpful.
  3. If I end up saving my employee keys inside epicor then I can skip the get employees call, and just make the get punches call. How do I make an auth call to the API without specifying a GET/POST call? I don’t see a call specifically for auth in my API documentation.

Yes, Yes it did.

One sec I’ll pull out how to do it yourself

1 Like
  Func<string, string, string> GetBasicAuthHeader = (username, password) =>
  {
      return "Basic " + Convert.ToBase64String(System.Text.UTF8Encoding.UTF8.GetBytes($"{username}:{password}"));  
  };
1 Like

I keep mine in UD Codes, so it’s not directly visible.

You can encrypt them too, but still easy to go get if you are inside the system.

try
{
    byte[] key = System.Text.ASCIIEncoding.ASCII.GetBytes("ShitShitShitShit");
    byte[] iv = System.Text.ASCIIEncoding.ASCII.GetBytes("FireFireFireFire");
    
    output1 = Epicor.Security.Cryptography.Encryptor.EncryptToBase64String("Hello World!", key, iv);
    
    output2 = Epicor.Security.Cryptography.Encryptor.DecryptToString(output1, key, iv);
}
catch (Exception ex)
{
    output1 = ex.Message;
}
2 Likes

Exactly. The current happy pattern is to have an API that makes the call for you. The API checks your authorization for the call, just plain o’ Entra, and if you have access to the function, it looks up the API Key from a vault (many out there) and makes the call with the API key on your behalf. The key is never seen by the client. This pattern is called a “Backend For Frontend” or BFF. Once you have that API, you can do other cool tricks like caching and further data restrictions by user. Makes it really fast and easy to rotate keys in case someone accidentally pushes one up into source control.

1 Like

I am embarrassed to say I don’t know how to use this.

Don’t be. You don’t have to. The relevant code is inside it.

However, you’ll probably want to learn how to use it.

That Func<T> thing is called a delegate, specifically a generic one that is built in to C#.

You can use them to define “Functions” inside procedures, like BPMs.
Like so:

//Func<inputType(string), returnType(string)>
Func<string, string> SayHi = (inString) =>
{
    return $"You said {inString}";
};

string retString = SayHi("Hello");

InfoMessage.Publish(retString);

There is also Action<T> which is like a void method (no return)

//Action<inputType(string)>
Action<string> SayHi2 = (inString) =>
{
    InfoMessage.Publish(inString);
};

SayHi2("Yo!");

Caveat: These are evaluated in order in your code, so you have to define them before you use them:

//This will throw an error because it is not defined yet. (just like a variable, because it IS a variable, with a function delegate in it)
SayHi2("Yo!");


//Action<inputType(string)>
Action<string> SayHi2 = (inString) =>
{
    InfoMessage.Publish(inString);
};

//This works
SayHi2("Yo!");
2 Likes