One of many cool things about Power Query is the way that it allows you to retrieve data from web services and load it into Excel. While this is a subject that lots of people (including me) have already blogged about, the Web.Contents() function – which is the M function that you’ll need to use to call a web service in your query – has gained some new, useful functionality over the last few releases that isn’t fully documented anywhere and which is important to know about.
For this post I’ll use a real-life example of a web service that I’ve been working with recently. As you may know, I’m one of the organisers of SQLBits and a few weeks ago I was given the task of building a Power BI solution to monitor registrations. SQLBits uses RegOnline to handle registrations and they have an API that allows developers to access registration data for events. I’ve been using this API in Power Query.
To take a simple example that illustrates the new functionality in Web.Contents(), take a look at the documentation for RegOnline’s GetEvents method:
When calling this method in Power Query using Web.Contents() there are two things I need to do:
- Pass two query parameters to it, called filter and orderBy – although I only need to pass empty values to these parameters
- Handle authentication by passing an API token either through another query parameter called APIToken or via an HTTP header also called APIToken
Here’s one way of doing this by handling authentication using the APIToken query parameter:
Web.Contents( "https://www.regonline.com/api/default.asmx/GetEvents", [ Query=[#"filter"="",#"orderBy"="",#"APIToken"="insertAPITokenHere"] ])
Here’s the other way of using Web.Contents() to do this, by passing the API token through an HTTP header:
Web.Contents( "https://www.regonline.com/api/default.asmx/GetEvents", [ Query=[ #"filter"="", #"orderBy"=""], Headers=[#"APIToken" = "insertAPITokenHere"] ])
To explain what’s going on in this second example, the second parameter to Web.Contents() is a record (so it has to be enclosed in square brackets) containing two options fields, Query and Headers. The Query field is itself a record, with one field for each of the two query parameters I’m passing. The Headers field is another record, with one field for each custom HTTP header I need (see this post on the Power Query forum for more details). The RegOnline API specifies that for the purposes of authentication a header is needed called APIToken which contains the API token, so the Headers option here contains one field for that header.
The problem with handling authentication as I do in both these examples is that I need to hard-code the RegOnline API token in the code for my Power Query query, which is not exactly secure because anyone who can see the code for my query can see the API token.
Instead, Power Query gives you the option of storing the API token in its own secure credentials store on your own PC (the same one that it uses for storing the usernames and passwords you enter for all other data sources). This only works if authentication takes place through a query parameter, as in the first example above – unfortunately it can’t be used if authentication needs to take place via a HTTP header as in the second example.
To use this technique I have to use the ApiKeyName option to tell Power Query the name of the query parameter that should contain the RegOnline API token:
Web.Contents( "https://www.regonline.com/api/default.asmx/GetEvents", [ Query=[ #"filter"="", #"orderBy"=""], ApiKeyName="APIToken" ])
Now, when the Power Query query is executed for the first time, you get prompted to tell Power Query how to authenticate the GetEvents method and in the dialog you have to choose Web API and enter the API token in the Key box, as shown below:
After this the API token gets stored separately from the query in the secure credentials store and you don’t need to enter it again each time the query runs; this also means that the API token will not travel with the workbook if you email it to someone else, for example.
One last thing to mention (and this is something that deserves a separate blog post really) is that while Web.Contents() generates a GET request by default, you can make it generate a POST request by specifying the Content option. You can find details on how to do this on this thread started by Lee Hawthorn on the old Data Explorer forum.