Take your Power Platform Custom Connectors to the next level.
Custom Connectors allow you to expose external APIs and services in a reusable way within the Microsoft Power Platform ecosystem. Using this component, an integration can be consumed from different experiences, such as Power Apps, Power Automate, or Copilot Studio agents , without each solution having to directly implement the API communication logic.
This makes the Custom Connector an integration piece that not only connects systems, but also allows you to encapsulate the technical complexity of an API and offer an interface ready to be reused by different solutions and user experiences.
But what happens when the API we need to work with doesn't exactly match what Power Platform allows us to configure by standard?
This is where Custom Connectors stop being just configuration and start becoming a development tool .
Beyond a standard connector.
A Custom Connector allows you to easily expose an API within Power Platform, but its possibilities don't end with defining endpoints, parameters, and authentication.
We can extend its behavior by:
Policies , using the templates available on Power Platform.
Custom C# code for scenarios that require additional transformation or logic.
This combination allows us to adapt the connector to both the API requirements and the experience we want to offer the end user.
Policies: small changes with a big impact.
Policies allow you to modify certain connector behaviors without needing to develop code.
Among the available options we find, for example:
Dynamically set the host URL.
Add or modify HTTP headers.
Set query properties or parameters.
Routing requests.
Transforming matrices and objects.
Convert delimited strings into arrays of objects.

A common scenario is working with different hosts for different environments. We can configure the connector so that the user specifies the corresponding host when creating the connection, avoiding the need to create separate connectors for development, sandbox, and production.

Another common scenario occurs when an API expects a specific format in its headers. For example, when it needs to receive an API Key or a Bearer token like this:
apikey YOUR_API_KEYInstead of forcing the user to know that format, we can use a policy that automatically adds the necessary prefix.

The result is a connector that is much easier to use and closer to the experience we expect from a component for a maker.
When policies aren't enough: custom code.
There are scenarios where the available templates are not sufficient.
For these cases, Power Platform allows incorporating custom C# code , which gives us greater control over the connector's requests and responses.
For example, we can transform an API response before returning it to Power Apps, Power Automate, or Copilot Studio.
This is especially interesting when an API returns a complex structure, but the user only really needs to work with a small part of that information.
public class Script : ScriptBase
{
public override async Task<HttpResponseMessage> ExecuteAsync()
{
// Check if the operation ID matches what is specified in the OpenAPI definition of the connector
if (this.Context.OperationId == "Converter")
{
return await this.HandleForwardAndTransformOperation().ConfigureAwait(false);
}
// Handle an invalid operation ID
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest);
response.Content = CreateJsonContent($"Unknown operation ID '{this.Context.OperationId}'");
return response;
}
private async Task<HttpResponseMessage> HandleForwardAndTransformOperation()
{
// Use the context to forward/send an HTTP request
HttpResponseMessage response =
await this.Context.SendAsync(this.Context.Request, this.CancellationToken)
.ConfigureAwait(false);
// Do the transformation if the response was successful, otherwise return error responses as-is
if (response.IsSuccessStatusCode)
{
var responseString =
await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// Example case: response string is some JSON object
var result = JObject.Parse(responseString);
var newResult = new JObject
{
["timestamp"] = result["info"]["timestamp"],
["date"] = result["date"],
["fromcurrency"] = result["query"]["from"],
["tocurrency"] = result["query"]["to"],
["fromamount"] = result["query"]["amount"],
["toamount"] = result["result"],
["exrate"] = result["info"]["rate"]
};
response.Content = CreateJsonContent(newResult.ToString());
}
return response;
}
}We can also correct problems with the call itself.
Custom code can also be used to resolve problems that arise during communication with certain APIs.
An example is the incorrect encoding of certain characters in the URL.
If the Custom Connector encodes a URL in a way that prevents the API from correctly interpreting the request, we can retrieve the generated URL, apply the necessary transformation, and continue with the call.
public class Script : ScriptBase
{
public override async Task<HttpResponseMessage> ExecuteAsync()
{
// get current request Uri
var strRequestUri = this.Context.Request.RequestUri.AbsoluteUri;
var strRequestUriDecode = HttpUtility.UrlDecode(strRequestUri);
this.Context.Request.RequestUri = new Uri(strRequestUriDecode);
HttpResponseMessage response =
await this.Context.SendAsync(
this.Context.Request,
this.CancellationToken)
.ConfigureAwait(false);
return response;
}
}Again, the complexity is encapsulated within the connector and not in the platform used.
When the authentication is not supported?
One of the most interesting scenarios arises when the API uses an authentication method that is not directly supported by Custom Connectors .
An example is the Client Credentials -based flow.
In these cases we can combine connection parameters, policies and C# code to build a workaround.
The pattern, simply put, consists of:
Incorporate the necessary parameters for authentication into the connector (Client Id, Client Secret and endpoint host).
We will download our connector files and manually add the connection parameters to apiProperties.json.
{
“properties”: {
“connectionParameters”: {
“token”: {
“type”: “string”,
“uiDefinition”: {
“displayName”: “token”,
“description”: “Token for the selected environment”,
“tooltip”: “Provide the token”,
“constraints”: {
“required”: “true”
}
}
},
“authType”: {
“type”: “string”,
“allowedValues”: [
{
“value”: “none”
}
],
“uiDefinition”: {
“displayName”: “Tipo de autenticacion”,
“description”: “Tipo de autenticacion para conectarse a la API”,
“tooltip”: “Tipo de autenticacion para conectarse a la API”,
“constraints”: {
“tabIndex”: 1,
“required”: “true”,
“allowedValues”: [
{
“text”: “none”,
“value”: “anonymous”
}
],
“capability”: [
“gateway”
]
}
}
},
“gateway”: {
“type”: “gatewaySetting”,
“gatewaySettings”: {
“dataSourceType”: “CustomConnector”,
“connectionDetails”: []
},
“uiDefinition”: {
“constraints”: {
“tabIndex”: 4,
“required”: “true”,
“capability”: [
“gateway”
]
}
}
}
},
“iconBrandColor”: “#007ee5”,
“capabilities”: [
“gateway”
],
“policyTemplateInstances”: [
{
“templateId”: “setheader”,
“title”: “Auth”,
“parameters”: {
“x-ms-apimTemplateParameter.name”: “Authorization”,
“x-ms-apimTemplateParameter.value”: “apikey @connectionParameters(‘token’)”,
“x-ms-apimTemplateParameter.existsAction”: “override”,
“x-ms-apimTemplate-policySection”: “Request”
}
}
],
“publisher”: “Mar Pedroche”
}
}Use policies to transfer those values to the headers.
As we have seen in the previous examples.
Add custom code that does the following:
Use custom code to obtain the token.
Remove the headers used during the authentication process.
Add only the Authorization header with the obtained token.
Finally, execute the call against the API.
public class Script : ScriptBase
{
public override async Task<HttpResponseMessage> ExecuteAsync()
{
// Obtener el token desde el proveedor OAuth2 usando Client Credentials
var accessToken = await GetAccessTokenAsync();
// Agregar el token al encabezado Authorization
Context.Request.Headers.Remove("authorization");
Context.Request.Headers.Remove("ClientId");
Context.Request.Headers.Remove("ClientSecret");
Context.Request.Headers.TryAddWithoutValidation(
"authorization",
"Bearer " + accessToken);
Context.Logger.LogInformation($"accessToken: {accessToken}");
Context.Logger.LogInformation(
$"url: {Context.Request.RequestUri?.ToString()}");
// Enviar la solicitud original con el token
var response = await Context.SendAsync(
Context.Request,
CancellationToken)
.ConfigureAwait(false);
return response;
}
private async Task<string> GetAccessTokenAsync()
{
string clientId = "";
string clientSecret = "";
// Obtener los valores desde los parámetros de conexión
if (Context.Request.Headers.TryGetValues("ClientId", out var clientIdValue))
{
clientId = clientIdValue.FirstOrDefault()?.ToString();
}
if (Context.Request.Headers.TryGetValues("ClientSecret", out var secretIdValue))
{
clientSecret = secretIdValue.FirstOrDefault()?.ToString();
}
Context.Logger.LogInformation(
$"clientID: {clientId}, clientSecret: {clientSecret}");
string tokenEndpoint =
"https://tokenexample.com/api/v1/oauth/token";
string scope = "myscope";
// Configurar la solicitud para obtener el token
var tokenRequest = new HttpRequestMessage(
HttpMethod.Post,
tokenEndpoint)
{
Content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>(
"grant_type",
"client_credentials"),
new KeyValuePair<string, string>(
"client_id",
clientId),
new KeyValuePair<string, string>(
"client_secret",
clientSecret),
new KeyValuePair<string, string>(
"scope",
scope)
})
};
// Enviar la solicitud al endpoint de token utilizando Context.SendAsync
var tokenResponse = await Context.SendAsync(
tokenRequest,
CancellationToken)
.ConfigureAwait(false);
if (!tokenResponse.IsSuccessStatusCode)
{
throw new Exception(
$"Error al obtener el token. Código de estado: {tokenResponse.StatusCode}, Razón: {tokenResponse.ReasonPhrase}");
}
// Leer y extraer el token manualmente desde la respuesta JSON
var tokenResponseContent =
await tokenResponse.Content.ReadAsStringAsync();
return ExtractAccessTokenFromResponse(tokenResponseContent);
}
private string ExtractAccessTokenFromResponse(string responseContent)
{
// Extraer el valor del campo "access_token" usando manipulación de cadenas
var tokenKey = "\"access_token\":\"";
var startIndex = responseContent.IndexOf(tokenKey);
if (startIndex == -1)
{
throw new Exception(
"El campo 'access_token' no se encontró en la respuesta del token.");
}
startIndex += tokenKey.Length;
var endIndex = responseContent.IndexOf("\"", startIndex);
if (endIndex == -1)
{
throw new Exception(
"El valor del campo 'access_token' no está bien formado.");
}
return responseContent.Substring(
startIndex,
endIndex - startIndex);
}
}
In this way, we were able to adapt the behavior of the Custom Connector to an authentication scenario that could not initially be configured directly.
Additionally, during development we can use Context.Logger.LogInformation to add traces and later consult the results from the connector's Code Logs.
The goal is not to "insert code for the sake of injecting code"
The ability to use C# within a Custom Connector doesn't mean we should turn every integration into a complex development project. This is especially true because it has time and size limits (5 seconds and 1 MB).
The goal is precisely the opposite: to use code to encapsulate complexity and offer a simplified experience to the connector's consumer .
A good Custom Connector should hide details such as:
Specific authentication formats.
Payload transformations.
Complex response structures.
Particularities of an API.
Coding problems.
Environment-specific configurations.
Custom Connectors as a bridge between development and business.
This is precisely one of the great advantages of Custom Connectors within a Power Platform strategy.
The technical team can handle the complexity of the integration, while maker users can consume them from various sources such as Copilot Studio, Power Automate, and Power Apps.
Because taking Power Platform to the next level doesn't always mean writing more code.
Sometimes it means writing the code necessary to make components reusable by any user .





Comments