Tuesday, 1 July 2025

Integrating Dynamics 365 with External APIs: Sending Data via Custom Actions and Plugins

 In the fast-paced world of business applications, integration between different systems is crucial for achieving streamlined operations and delivering comprehensive solutions. Dynamics 365, a powerful suite for managing customer relationships, often needs to interact with external APIs to exchange data, automate workflows, and extend functionality. In this blog post, we’ll walk through the process of integrating Dynamics 365 with external APIs using custom actions and plugins, ensuring your applications are both functional and efficient.

Integrating Dynamics 365 with external APIs allows businesses to:

  • Automate Data Exchange: Reduce manual data entry by automatically pushing or pulling data between Dynamics 365 and other systems.
  • Enhance Functionality: Expand the capabilities of Dynamics 365 by leveraging services and features available through external APIs.
  • Improve User Experience: Create seamless workflows that combine the strengths of multiple applications, offering a more cohesive experience.

Overview of the Integration Process:

1.Add the Button to the Ribbon (Command Bar): To initiate the integration, you’ll need to add a button labeled Send Details to XYZ to the ribbon (command bar) of your Dynamics 365 form. This button will trigger the entire process. You can add the button using the Ribbon Workbench, a popular tool for customizing the Dynamics 365 apps. In this guide, we’ll use the Ribbon Workbench for simplicity.

fig-1: Adding button using ribbon workbench
fig-2: Button added in the form

2.Create the Custom Action: Next, create a custom action in Dynamics 365 that will be linked to the button you added in the previous step. This custom action will trigger the plugin that handles the API integration. You can create this action directly through the Dynamics 365 interface or using the Power Platform. This step is crucial because it serves as the bridge between the button click and the backend logic handled by the plugin.

Fig-3: Adding custom action to trigger the plugins

3. Write the JavaScript to Trigger the Custom Action: To make the button functional, create a JavaScript Web Resource in Dynamics 365. This script will trigger the custom action whenever the button is clicked. Below is a sample JavaScript code that you can use. It retrieves necessary data (like the Opportunity ID), and then calls the custom action.

Here’s a sample JavaScript code that you can use:

function sendDetailsToXYZ (executionContext) {
        var opportunityId = Xrm.Page.data.entity.getId().replace(/[{}]/g, "");
        // console.log("Opportunity ID:", opportunityId);
        
        var confirmStrings = {
            text: "Do you want to create a client record in XYZ? Please confirm.",
            title: "Confirmation Dialog",
            confirmButtonLabel: "Confirm",
            cancelButtonLabel: "Cancel"
        };
        var confirmOptions = {
            height: 200,
            width: 450
        };
        
        Xrm.Navigation.openConfirmDialog(confirmStrings, confirmOptions).then(
            function (success) {
                if (success.confirmed) {
                    console.log("Running Action....");
                    var globalContext = Xrm.Utility.getGlobalContext();
                    var serverURL = globalContext.getClientUrl();
                    var actionName = "bh_CustomActionToPluginSendDetailsToVCC";
                    var data = {
                        "MyInputParam" : opportunityId
                    };
                    var req = new XMLHttpRequest();
                    req.open("POST", serverURL + "/api/data/v9.2/" + actionName, true);
                    req.setRequestHeader("Accept", "application/json");
                    req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
                    req.setRequestHeader("OData-MaxVersion", "4.0");
                    req.setRequestHeader("OData-Version", "4.0");
                    req.onreadystatechange = function () {
                        if (this.readyState == 4 /* complete */)
                        {
                            req.onreadystatechange = null;
                           
                            if (this.status == 200 || this.status == 204)
                            {
                                console.log("Action Called Successfully...");
                                var result = JSON.parse(this.response);
                                var pluginResponse = result.MyOutputParam;
                                console.log(pluginResponse);
                               if(result.MyOutputParam == "Client record has been created in XYZ"){
                                var alertStrings = {
                                    confirmButtonLabel: "OK",
                                    text: `${pluginResponse}`,
                                    title: "Success"
                                };
                                var alertOptions = {
                                    height: 120,
                                    width: 260
                                };
                                try {
                                    Xrm.Navigation.openAlertDialog(alertStrings, alertOptions);
                                } catch (error) {
                                    console.error(`Error displaying alert: ${error.message}`);
                                }
                               }
                               else{
                                var errorAlertStrings = {
                                    confirmButtonLabel: "OK",
                                    text: `${pluginResponse}`,
                                    title: "Error"
                                };
                                var alertOptions = {
                                    height: 120,
                                    width: 260
                                };
                                try {
                                    Xrm.Navigation.openAlertDialog(errorAlertStrings, alertOptions);
                                } catch (error) {
                                    console.error(`Error displaying alert: ${error.message}`);
                                }
                               }
                            }
                            else
                            {
                                var error = JSON.parse(this.response).error;
                                console.log("Error in Action: "+error.message);
                            }
                        }
                    };
                    //Execute request passing the input parameter of the action 
                    req.send(window.JSON.stringify(data));
    
                } else {
                    console.log("Dialog closed using Cancel button or X.");
                }
            }
        );
    }

4. Develop a Plugin to Handle the Logic: The plugin will be responsible for fetching data from Dynamics 365 and sending it to the external API. This ensures that the data exchange is secure, reliable, and efficient. The plugin is triggered when the custom action is executed, which is accomplished by creating a synchronous, post-operation, server-side step.

using System;
using System.Collections.Generic;
using System.IdentityModel.Protocols.WSTrust;
using System.Linq;
using System.Runtime.Remoting.Contexts;
using Microsoft.Xrm.Sdk;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Microsoft.Xrm.Sdk.Query;

namespace XYZCRMPlugin
{
    public class SendDetailToXYZ : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

            string environmentVariableSchemaName = "bh_vytaapiaccesskey";
            // Retrieve the Environment Variable Definition record
            QueryExpression Apiquery = new QueryExpression("environmentvariabledefinition")
            {
                ColumnSet = new ColumnSet("environmentvariabledefinitionid", "defaultvalue")
            };
            Apiquery.Criteria.AddCondition("schemaname", ConditionOperator.Equal, environmentVariableSchemaName);

            EntityCollection envVarDefResults = service.RetrieveMultiple(Apiquery);
            string apiKey = null; // Initialize the apiKey variable
            string stateCodeValue = null;
            if (envVarDefResults.Entities.Count > 0)
            {
                Entity envVarDef = envVarDefResults.Entities[0];
                Guid envVarDefId = envVarDef.Id;

                // Try to get the current value of the environment variable
                QueryExpression valueQuery = new QueryExpression("environmentvariablevalue")
                {
                    ColumnSet = new ColumnSet("value")
                };
                valueQuery.Criteria.AddCondition("environmentvariabledefinitionid", ConditionOperator.Equal, envVarDefId);

                EntityCollection envVarValResults = service.RetrieveMultiple(valueQuery);

                if (envVarValResults.Entities.Count > 0)
                {
                    Entity envVarVal = envVarValResults.Entities[0];
                    apiKey = envVarVal.GetAttributeValue<string>("value");
                }

                // If no current value is found, use the default value
                if (string.IsNullOrEmpty(apiKey))
                {
                    apiKey = envVarDef.GetAttributeValue<string>("defaultvalue");
                }

                // Use the environment variable value as needed
                tracingService.Trace($"Environment Variable Value: {apiKey}");
            }
            else
            {
                tracingService.Trace("Environment Variable Definition not found.");
            }

            var query = new QueryExpression("bh_systemsetting")
            {
                ColumnSet = new ColumnSet("bh_value")
            };
            query.Criteria.AddCondition("bh_name", ConditionOperator.Equal, "CREATE_CLIENT_XYZ_API_URL");

            var apiUrlEntity = service.RetrieveMultiple(query).Entities.FirstOrDefault();
            if (apiUrlEntity == null || !apiUrlEntity.Contains("bh_value"))
            {
                throw new InvalidPluginExecutionException("API URL not found in system settings.");
            }

            string apiUrl = apiUrlEntity["bh_value"].ToString();
            tracingService.Trace($"API URL: {apiUrl}");

            string opportunityId = (string)context.InputParameters["MyInputParam"];
            tracingService.Trace($"OpportunityID: {opportunityId}");

            if (!string.IsNullOrEmpty(opportunityId))
            {
                try
                {
                    // Define the column you want to retrieve from opportunity
                    ColumnSet opportunityColumns = new ColumnSet("parentcontactid", "bh_quotestringfromxyz");

                    // Retrieve the opportunity record
                    Entity opportunity = service.Retrieve("opportunity", new Guid(opportunityId), opportunityColumns);

                    // Extract the 'parentcontactid' field from the opportunity record
                    EntityReference parentContact = opportunity.Contains("parentcontactid") ? (EntityReference)opportunity["parentcontactid"] : null;

                    string quoteString = opportunity.Contains("bh_quotestringfromxyz") ? opportunity["bh_quotestringfromxyz"].ToString() : string.Empty;

                    if (parentContact != null)
                    {
                        // Define the columns you want to retrieve from contact
                        ColumnSet contactColumns = new ColumnSet(
                            "firstname",
                            "lastname",
                            "emailaddress1",
                            "telephone1",
                            "address1_city",
                            "bh_stateid",
                            "address1_line1",
                            "address1_postalcode",
                            "address1_line2"
                        );

                        // Retrieve the contact record
                        Entity contact = service.Retrieve("contact", parentContact.Id, contactColumns);

                        // Extract the fields from the contact record
                        string firstName = contact.Contains("firstname") ? contact["firstname"].ToString() : string.Empty;
                        string lastName = contact.Contains("lastname") ? contact["lastname"].ToString() : string.Empty;
                        string email = contact.Contains("emailaddress1") ? contact["emailaddress1"].ToString() : string.Empty;
                        string telephone = contact.Contains("telephone1") ? contact["telephone1"].ToString() : string.Empty;
                        string city = contact.Contains("address1_city") ? contact["address1_city"].ToString() : string.Empty;
                       // string stateOrProvince = contact.Contains("address1_stateorprovince") ? contact["address1_stateorprovince"].ToString() : string.Empty;
                        string addressLine1 = contact.Contains("address1_line1") ? contact["address1_line1"].ToString() : string.Empty;
                        string postalCode = contact.Contains("address1_postalcode") ? contact["address1_postalcode"].ToString() : string.Empty;
                        string addressLine2 = contact.Contains("address1_line2") ? contact["address1_line2"].ToString() : string.Empty;

                        EntityReference stateId = contact.Contains("bh_stateid") ? (EntityReference)contact["bh_stateid"] : null;
                        if (stateId != null)
                        {
                            ColumnSet columnSet = new ColumnSet("bh_statename");
                            Entity stateEntity = service.Retrieve("bh_state", stateId.Id, columnSet);

                            if (stateEntity != null && stateEntity.Contains("bh_statename"))
                            {
                                stateCodeValue = stateEntity["bh_statename"].ToString();
                            }
                        }
                        // Trace the retrieved values
                        tracingService.Trace($"Parent Contact Details:");
                        tracingService.Trace($"First Name: {firstName}");
                        tracingService.Trace($"Last Name: {lastName}");
                        tracingService.Trace($"Email: {email}");
                        tracingService.Trace($"Telephone: {telephone}");
                        tracingService.Trace($"City: {city}");
                        tracingService.Trace($"State/Province: {stateCodeValue}");
                        tracingService.Trace($"Address Line 1: {addressLine1}");
                        tracingService.Trace($"Postal Code: {postalCode}");
                        tracingService.Trace($"Address Line 2: {addressLine2}");
                        tracingService.Trace($"Quote String: {quoteString}");

                        // Create the customer data
                        var customerData = new
                        {
                            crmID = opportunityId,
                            firstName = firstName,
                            lastName = lastName,
                            email = email,
                            phoneNumber = telephone,
                            quoteString = quoteString,
                            address = new
                            {
                                city = city,
                                province = stateCodeValue,
                                street = addressLine1,
                                unitNumber = addressLine2,
                                postalCode = postalCode,
                                aptType = "Apt" 
                            }
                        };

                        // Wrap the customer data in an array
                        var dataToSend = new[]
                        {
                            new
                            {
                                customerData = customerData
                            }
                        };

                        // Call the asynchronous method to make the API request
                        var response = PostToAPIAsync(dataToSend, apiUrl, apiKey, tracingService , service, parentContact.Id).Result;

                        // Set the API response as the output parameter
                        context.OutputParameters["MyOutputParam"] = response;
                    }
                    else
                    {
                        tracingService.Trace("Parent contact ID is null.");
                    }
                }
                catch (Exception ex)
                {
                    tracingService.Trace($"Exception: {ex.Message}");
                    throw;
                }
            }
            else
            {
                tracingService.Trace("Opportunity ID is null or empty.");
            }
        }
        private async Task<string> PostToAPIAsync(object data, string apiUrl, string apiKey, ITracingService tracingService , IOrganizationService service, Guid contactId)
        {
            try
            {
                using (HttpClient client = new HttpClient())
                {
                    client.DefaultRequestHeaders.Accept.Clear();
                    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    client.DefaultRequestHeaders.Add("x-api-key", apiKey);
                    client.DefaultRequestHeaders.Add("User-Agent", "VytaCRMPlugin");

                    string json = JsonConvert.SerializeObject(data);
                    HttpContent content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

                    HttpResponseMessage response = await client.PostAsync(apiUrl, content);

                    if (response.IsSuccessStatusCode)
                    {
                        string result = await response.Content.ReadAsStringAsync();
                        tracingService.Trace($"API call successful. Response: {result}");
                        // Deserialize the JSON response
                        var jsonResponse = JsonConvert.DeserializeObject<dynamic>(result);

                        // Access the vccID from the deserialized object
                        string vccId = jsonResponse.vccID;
                        tracingService.Trace($"Extracted vccID: {vccId}");
                        if (!string.IsNullOrEmpty(vccId))
                        {
                            // Update the contact with the new vccId
                            Entity updateContact = new Entity("contact", contactId)
                            {
                                ["bh_vcccontactid"] = vccId
                            };

                            // Update the contact record
                            service.Update(updateContact);

                            tracingService.Trace($"Contact updated with XYZ ID: {vccId}");
                        }
                        else
                        {
                            tracingService.Trace("XYZ ID was not returned from API.");
                        }
                        return "Client record has been created in XYZ "; // Return the response content
                    }
                    else
                    {
                        string error = $"API call failed. Status code: {response.StatusCode}, Reason: {response.ReasonPhrase}";
                        tracingService.Trace(error);
                        return $"There was an error occurred while creating client record in XYZ . Please contact administrator. Error Message:{error} "; // Return the error message
                    }
                }
            }
            catch (Exception ex)
            {
                string exceptionMessage = $"Exception: {ex.Message}";
                tracingService.Trace(exceptionMessage);
                throw; // Optionally re-throw the exception if you want it to bubble up
            }
        }
    }
 }
Fig-4: Register steps in the plugin when the plugin is registered.

Conclusion

Integrating Dynamics 365 with external APIs through custom actions and plugins is a powerful way to extend your CRM’s capabilities, automate workflows, and enhance user experiences. By following the steps outlined in this post, you can create seamless connections between Dynamics 365 and other systems, ensuring your business processes are as efficient and effective as possible.

Whether you’re looking to automate data entry, enhance functionality, or improve user experience, these integrations are key to unlocking the full potential of Dynamics 365. Start implementing these steps today and see the impact on your projects!

No comments:

Post a Comment

No lock and distinct conditions in query expression

  Hi Folks, Whenever i tried using query expression for retrieve multiple operations, came to know some interesting facts which exists and h...