I am brand new to APIs and am struggling to find comprehensive documentation. My goal is to run a post method to an external website to get freight quotes by passing through 9 parameters. I have all of the information I need to connect to the url, but I’m a bit confused on how to pass the necessary parameters. Does anyone have any documentation that will help me to understand this whole process? Sorry if I’m using incorrect terminology - please feel free to educate me!
https://yourserver/yourenvironment/api/help/v1
The generated Swagger docs are better than the formal documentation. They’re a great resource for working with business objects in BPMs, too. Change v1 to v2 for the v2 docs. These docs will tell you whether each method is GET, POST, etc., and you can play around with what happens with different parameter values. Is that what you mean by how to pass the parameters?
Doesn’t that documentation only outline how to call APIs within Epicor’s business library? I’m trying to connect to an outside company’s API; they sent me the documentation for what the request payload will look like. By parameters, I mean the nodes in the request payload.
Ah, I totally misunderstood you question. Are you asking how to make HTTP requests in general? What kind of program will be making these requests? I mean, are you thinking of making these requests as needed from inside Epicor, or from an external program like a Windows service?
I’m sitting in a coffee shop waiting for someone to get back to me about why our VPN isn’t working, so I’ll hazard a guess…
If you want to make this request when something happens in Epicor, turn on trace logging and find out which business object methods are called during the process that you want to trigger the request. Sometimes there’s a method that’s obviously specifically related to the thing you’re interested in, like OnChangeWhatever
. Other times you might have to use a more generic method like Update
and add conditions to check which fields changed.
You might want to use a post-processing BPM and update the data being returned from the BO method with whatever information you get from the external API. I’ve never done anything in a BPM that’s more time-consuming than accessing the database. If you make the HTTP request synchronously, the user might perceive the delay in the UI. But if you try to spin off a background task, you may run into problems like the DbContext
that the BPM is using going out of scope and being disposed before the request completes.
To add onto Kevin’s in-depth response, here’s an example of me calling an external web service via a BPM (well, inside an Epicor Function) by reading a row from a UD table. With the response from your web service, you’d then be able to further do something with the data i.e. write it to a record or whatever.
//call EE API with the event data needing to be processed
string codeident = $"Function: EpicorBridgeInternal.Events-ProcessEvents ByTrigger {DateTime.Now.ToString()}";
//Authorization Key
string authKey = "REDACTED"; //01-14-2021
string serviceHost = $"https://{host}/webservice/rest/update_entry/php?auth[shortkey]={authKey}&data[site_id]=1";
wasSuccessful = false;
msg = "";
/*
*/
//Ice.Diagnostics.Log.WriteEntry(codeident);
//process record here
if (tsUD05.UD05 !=null)
{
//deserialize contents
dynamic contents = JsonConvert.DeserializeObject(tsUD05.UD05[0].Character01);
string epiNum = contents.id;
//change resource based on endpoint function
string endpoint = tsUD05.UD05[0].Key2;//endpoint names: update_shipping, update_status
string resource = "";
switch(endpoint){
//Update Sold To/Ship To Name
case "update_custconfig":
string newSoldToName = contents.newSoldTo;
string newShipToName = contents.newShipTo;
string encodeSoldTo = WebUtility.UrlEncode(newSoldToName);
string encodeShipTo = WebUtility.UrlEncode(newShipToName);
resource = $"&data[search][epinum]=={epiNum}&data[sold_to_name]={encodeSoldTo}&data[ship_to_name]={encodeShipTo}";
break;
//Update Allocation Status
case "update_allocation":
string allocated = contents.body;
resource = $"&data[search][epinum]=={epiNum}&data[allocated]={allocated}";
break;
//Update Shipping
case "update_shipping":
string trackingNum = contents.body;
resource = $"&data[search][epinum]=={epiNum}&data[tracking]={trackingNum}";
break;
//Update Status
case "update_status":
string status = "";//"H" = Hold, "A" = Active, "C" = Completed, "CC" = Cancelled/Lost
string newStatus = contents.new_status;
//this.PublishInfoMessage(" New Status: " + newStatus, Ice.Common.BusinessObjectMessageType.Information, Ice.Bpm.InfoMessageDisplayMode.Individual, "", "");
switch(newStatus){
case "A": status = "Active";
break;
case "C": status = "Completed";
break;
case "CC": status = "Cancelled";
break;
case "H": status = "Hold";
break;
default: status = "";//a blank status will error on the site
break;
}
resource = $"&data[search][epinum]=={epiNum}&data[status]={status}";
//debug
//this.PublishInfoMessage("constructedURL: " + $"{host}{resource}", Ice.Common.BusinessObjectMessageType.Information, Ice.Bpm.InfoMessageDisplayMode.Individual, "", "");
break;
default:
break;
}
//Call the service
var request = (HttpWebRequest)WebRequest.Create($"{serviceHost}{resource}");
request.Method = "GET";
//listen to response
try
{
var response = (HttpWebResponse)request.GetResponse();
switch(response.StatusCode)
{
case HttpStatusCode.OK:
using (var streamReader = new StreamReader(response.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
if(result.Contains("1"))//successful
{
wasSuccessful = true;
msg = "Successful connection and record successfully updated on portal";
url = $"{serviceHost}{resource}";
}
else
{
wasSuccessful = true;
msg = "Successful connection, but no matching record on portal";
url = $"{serviceHost}{resource}";
}
//debug
//wasSuccessful = true;
//msg = "Successful connection, but no matching record on portal";
//this.PublishInfoMessage(" Response: " + result, Ice.Common.BusinessObjectMessageType.Information, Ice.Bpm.InfoMessageDisplayMode.Individual, "", "");
}
break;
}
}
catch(Exception ex)
{
//this.PublishInfoMessage(codeident + "Bad Response: " + ex.Message + ex.InnerException + $" {serviceHost}{resource}", Ice.Common.BusinessObjectMessageType.Information, Ice.Bpm.InfoMessageDisplayMode.Individual, "", "");
//set variables for UD05 rec update for "fail"
wasSuccessful = false;
msg = "Connection Failure-unable to get an OK response";
url = $" {serviceHost}{resource}";
}
}
Obviously your external web service will dictate the response behavior but this is intended to give you an idea about the mechanisms needed to call a web service inside a BPM and work with a response.
I found myself writing code to call external APIs so often that I just created a dedicated library/function for utilities like this. I pass in the URL, method, JSON payload, etc to the function. Much easier to call a function for this than using custom code each time.
I second @fvodden on this. Encapsulating your setup in a function makes it clean an reusable.
That said, here’s my first attempt at a SalesForce API call (before you wonder if I’m nuts all the client secrets etc are obfuscated)
We actually went live using this but in Func delegates as we unfortunately didn’t have Efx functions yet. Working on redoing them in efx Functions this summer.
That’s a really great example. Thank you! One question though, it appears you’re passing all parameters through a constructed URL to make the API call. How would you handle it if you couldn’t pass these values through the URL? Based on what I’m seeing, it looks like serialization may be the way to handle this, as I have multiple parent-child nodes in my JSON file.
In this example I am indeed passing the parameters as query parameters. If your API consumes from the body of the call, you could wrap up the parameters and add them to the body. I have an example (not Epicor but using C#) below adding parameters to the body of an API call using the RestSharp library.
/// <summary>
/// Invokes an Epicor Function from the specific function library
/// </summary>
/// <param name="library">The Library ID associated with the function</param>
/// <param name="functionID">The Function ID to be invoked</param>
/// <param name="fxRequest">The JSON from the call body representing the optional input parameters</param>
/// <returns></returns>
public async Task<IActionResult> InvokeFunction(string library, string functionID, dynamic fxRequest)
{
if (_epiUtils.ValidSession(_epiUtils.sessionID, _licenseType, _path, _user, _apiKey, out string msg))
{
var restClient = new RestClient(_fxPath)
{
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true
};
var request = new RestRequest($"{library}/{functionID}", Method.POST);
//add any optional request parameters
request.AddParameter("application/json", fxRequest, ParameterType.RequestBody);
//Web Service License
var headerLicense = new
{
ClaimedLicense = _licenseType,
SessionID = _epiUtils.sessionID
};
var header = JsonConvert.SerializeObject(headerLicense);
request.AddHeader("License", header);
request.AddHeader("Authorization", $"Basic {EpiUtils.Base64Encode(_user)}");
request.AddHeader("x-api-key", _apiKey);
IRestResponse response = await restClient.ExecuteAsync(request);
switch (response.StatusCode)
{
case System.Net.HttpStatusCode.BadRequest:
{
dynamic content = JsonConvert.DeserializeObject(response.Content);
var value = content;
return BadRequest(content);
}
case System.Net.HttpStatusCode.OK:
default:
{
dynamic content = JsonConvert.DeserializeObject(response.Content);
var value = content;
return Ok(content);
}
}
}
else
{
return Unauthorized(msg);
}
}
In my first example, I am using a HttpWebRequest because Epicor doesn’t ship with RestSharp available out of the box in the server assemblies (), so I’m forced to use ancient tech to accomplish that. However, it is possible, just don’t have a great example:
C# JSON Post using HttpWebRequest - Stack Overflow
For your parameters, I don’t believe in doing hard-coded serialization like some people do but an easy way to accomplish that is to add your parameters to a Dictionary and serialize the whole thing into JSON :
//add custom field values to this dictionary
var customFields = new Dictionary<string,string>;
{
{"Physician","Dr. Test"}
,{"Patient", "Test Patient"}
,{"Graft Requested", "Femoral Core, 10mm"}
,{"Graft Offered", "Femoral Core, 10mm"}
,{"Graft ID", "123456-999"}
,{"Release Date", "10/18/2018"}
,{"Expire Date", "10/30/2018"}
,{"Patient Size Msg", "TW=N/A, W=N/A, L=N/A (bunch of text here)"}
,{"Donor Size Msg", "TW=N/A, W=N/A, L=N/A (bunch of text here)"}
,{"QuoteDtl Comment", "Please see dissection sheet for specific sizing"}
,{"ASC", "Aaron Moreng"}
,{"Offer Date", "10/18/2018"}
};
Or if you like even less code, you can just use an anonymous type to build your body and serialize it:
var taskHead = new
{
planId = bucket.PlanID,
bucketId = bucket.BucketID,
title = "New Task from Teams Bridge"
};
var content = JsonConvert.SerializeObject(taskHead);
Because you won’t have the ability to create a model for your response, I’d also recommend using the dynamic
type when reading your response, especially if you are accessing nested data within it.
RE: Nested json; you’ll be able to do this with ease using anonymous types. That’s definitely what I’d recommend for this.
Anonymous Types | Microsoft Docs
@josecgomez taught @jdewitt6029 and I about the dynamic types when we were trying to learn his nuget packages. I wasn’t the one doing the coding, I was watching over a shoulder mesmerized about dynamic types… I still have yet to try and use them in a solution.
If you have access to the embedded education there is an Advanced Epicor Functions course that has some examples of accessing external APIs like the ZipCodeAPI.
Thanks for adding this, I didn’t know that resource existed.
Apparently it is deprecated in later versions, but it is there in 10.2.700
They do that. They yanked a ton of videos using the classic interface on the Knowledge on Demand / Epicor Education platform. I was using those to onboard people but apparently we need to upgrade.