Register a HubSpot Integration App with Azure Functions

About Hubspot

Let’s start from the beginning. What is HubSpot? HubSpot is a Marketing-, Sales-, Service, CMS and Operations-Software. Learn more about HubSpot here.

What I like in particular with HubSpot is the integration possibilities it offers. It has a rich API and also the capability to call webHooks among other things.

However there are two ways to authenticate against the HubSpot API

  • API-Key
  • OAUTH

Hubspot itself disencourages you from using the API-Key other than for testing/developing purposes and given that an API-Key has no permissions attached to it and gives you control to almost everything in HubSpot you should never hand out the API-Key of a production system!

To develop applications you can register for a free developer account and sandbox for testing.

Concept and registration process of HubSpot Applications

HubSpot allows ISV’s (independent software vendors) to write integrations and offer those as „Hubspot-Integration-Apps“ to all customers of HubSpot. Therefore your integration always must be multi-tenant per se. Although there are some „workarounds“ to only allow access to certain HubSpot-Tenants as we see later.

As an ISV you register an „HubSpot-Application“ inside your Developer Account with HubSpot. Here you will gain access to a so called „Installation-Url“ that you can offer your customers to install you Applications in their HubSpot-Tenants. If you also publish your HubSpot-Application to the HubSpot-Store all customers will see this integration and can choose to install your application.

As you can see in the illustration below „your“ ISV Application from your Developer-Tenant is installed in the tenants of Customers A and B. Customer C has not installed „your“ ISV Application.

So how does the „Install-Process“ actually work conceptually? While registering your HubSpot-Application (this process is quite straight forward) you need to provide an End-Point that HubSpot can call once a customer decides to install the application to their tenant.

You also declare what permissions you require at a minimum so your integration works properly (For example: Access to Contacts)

If the user clicks on the installation url (which by the way can contain additional optional permissions) they have to confirm that they want to give access to the application.

Then the endpoint (ISV) simply will receive an „CODE“ that must be redeemed to get an Access Token from HubSpot. The base code template for an Azure Function App would look like this:

[FunctionName("RegisterHubSpotApp")]
public async Task<IActionResult> RegisterHubSpotApp(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] 
    HttpRequest req,
    ILogger log)
{
    try
    {
        var code = req.Query["code"].FirstOrDefault();
        if (!string.IsNullOrWhiteSpace(code))
        {
            // Redeem Code here!
        }
    }
    catch (Exception ex)
    {
        log.LogError(ex, ex.Message);
        return new BadRequestObjectResult(ex.Message);
    }
    return new BadRequestResult();
}

Unfortunately we still do not know which Customer Tenant did choose to install our application! The next step would be to acquire the Access-Token and Refresh-Token! Which still does not provide us with the information about the tenant!

Access-Token: This is the BEARER-Token you must use in the „Authorization„-Header when you make calls to the HubSpot-API. This will only be valid for a certain amount of time (see „ExpiresIn„-Field)

Refresh-Token: This is the token you must keep secure at all costs! Best to store it somewhere secure like Azure KeyVault. Having this token you can request new Access-Tokens that have expired! [SECURITY WARNING]

Said that to acquire the Access/Refresh-Token you need to provide following information that you can obtain from your registered HubSpot-Application:

  • ClientId
  • ClientSecret
  • RedirectUri (your Endpoint, where the Code was sent)

A simple implementation in an Azure Function might look like this:

public class OAuthTokenResponse
    {
        [JsonProperty(PropertyName = "token_type")]
        public string TokenType { get; set; }
        [JsonProperty(PropertyName = "refresh_token")]
        public string RefreshToken { get; set; }
        [JsonProperty(PropertyName = "access_token")]
        public string AccessToken { get; set; }
        [JsonProperty(PropertyName = "expires_in")]
        public int ExpiresIn { get; set; }
    }

private const string HUBSPOT_TOKEN_API = "https://api.hubapi.com/oauth/v1/token";

private async Task<OAuthTokenResponse> GetOAuthToken(string code)
{
    HttpClient client = _httpFactory.CreateClient();

    List<KeyValuePair<string, string>> data = new List<KeyValuePair<string, string>>()
        {
            new KeyValuePair<string, string>("grant_type","authorization_code"),
            new KeyValuePair<string, string>("client_id",_config["clientID"]),
            new KeyValuePair<string, string>("client_secret",_config["clientSecret"]),
            new KeyValuePair<string, string>("redirect_uri", _config["RedirectUri"]),
            new KeyValuePair<string, string>("code", code)
        };
    HttpRequestMessage httpRequest = new HttpRequestMessage
    {
        RequestUri = new Uri(HUBSPOT_TOKEN_API),
        Content = new FormUrlEncodedContent(data),
        Method = HttpMethod.Post
    };
    HttpResponseMessage httpResponse = await client.SendAsync(httpRequest);
    if (httpResponse.IsSuccessStatusCode)
    {
        var token = JsonConvert.DeserializeObject<OAuthTokenResponse>(await httpResponse.Content.ReadAsStringAsync());
        if (token != null && !string.IsNullOrWhiteSpace(token.RefreshToken))
        {
            return token;
        }
    }
    else
    {
        var response = await httpResponse.Content.ReadAsStringAsync();
    }
    return null;
}

So there is a lot of effort to be done and still we have no idea which Tenant has requested the install! Fortunately we can use the RefreshToken (keep in mind to keep it secure!) to retrieve some information about it.

The result of this API Call will look like this:

{
  "token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "user": "test@hubspot.com",
  "hub_domain": "demo.hubapi.com",
  "scopes": [
    "automation",
    "contacts",
    "oauth"
  ],
  "hub_id": 62515,
  "client_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
  "user_id": 123,
  "token_type": "refresh"
}

and gives us information about

  • the user who installed the application („user“, „user_id“)
  • the scopes we have available and can call apis for („scopes“)
  • the client-ID of our registered HubSpot-API (in case we have more than one integration app)
  • Finally the tenant-ID („hub_id“)

You might have noticed using Hubspot that the „hub_id“ is always a part of the URL. Example of an url to a contact:

An implementation in Azure Functions might look like this:

private async Task<OAuthRefreshTokenInfo> GetRefreshTokenInfo(string refreshToken)
{
    HttpClient client = _httpFactory.CreateClient();

    HttpResponseMessage tokenInfoResponse = await client.GetAsync(string.Format("https://api.hubapi.com/oauth/v1/refresh-tokens/{0}", refreshToken));
    if (tokenInfoResponse.IsSuccessStatusCode)
    {
        OAuthRefreshTokenInfo tokenInfo = JsonConvert.DeserializeObject<OAuthRefreshTokenInfo>(await tokenInfoResponse.Content.ReadAsStringAsync());
        if (tokenInfo.ClientId == _config["clientID"])
        {
            return tokenInfo;
        }
    }
    return null;
}

After we do have AccessToken, RefreshToken and Tenant-Information we need to store the Refresh-Token together with the Tenant-Information so our integration application can use this information to create an AccessToken on demand.

Make sure you cache the AccessToken securely to prevent unnecessary round-trips in your integration. And make sure the RefreshToken is stored securely (f.e. Azure KeyVault)!

Thanks and enjoy

Nina