Written By Rick de Groot

Rick is the founder of BI Gorilla. He believes learning is one of life's greatest pleasures and shares his knowledge to help you improve your skills.

Have you ever wanted to make API calls directly in Power Query? Well, you’re in luck! In this article, I’m going to show you how to use the powerful List.Generate function to make API calls in Power Query.

We’ll start by making an API call, then we’ll create a function using List.Generate to return the relevant fields. With this technique, you’ll be able to easily access data directly from an API, giving you some options to experiment with it.

Imke Feldman used this same API and showed a way to retrieve API data up to the last page. This post is more in-depth and shows two different approaches. I’ll make sure to explain everything in a simple and easy-to-understand way, so even if you’re a beginner, you’ll be able to follow along.

Table of contents
How to Use List Generate to Make API calls in Power Query

List.Generate

To get started with APIs, we will use the List.Generate function. List.Generate allows you to create a series of values based on logic. If you’ve never worked with List.Generate check out this article.

What makes List.Generate so powerful that it can perform a certain action until the provided condition returns false. This principle allows you to call an API, and keep firing requests until you reach the last record in the database. It is similar to a do-while loop, applying a function as long as a condition holds true. So how can you do that?

Make API Call

Imagine there’s a limit to the amount of records you can retrieve per API call. In each request, you can retrieve up to 100 records, and then move on to the next 100, and then the next, etc. You can give those instructions by using the limit and offset parameters in the API request. The first 4 requests look like:

https://pokeapi.co/api/v2/pokemon?limit=100&offset=0
https://pokeapi.co/api/v2/pokemon?limit=100&offset=100
https://pokeapi.co/api/v2/pokemon?limit=100&offset=200
https://pokeapi.co/api/v2/pokemon?limit=100&offset=300

The limit value stays the same for each request, whereas the offset increments to 100 each call. This requirement is very similar to using List.Generate for pagination, but in bigger steps.

So, how can we import the data of the first request into Power Query? You can start by creating a New Query, and choose From Web. Then, paste the first URL into the address bar.

List.Generate in Power Query

The request retrieves a JSON file. Power Query automatically recognizes the data format and adds the Json.Document function to the formula.

Record from Json file

The JSON file shows as a record in Power Query. From this record, you only need the data from the results column. You can return the list values by referencing the column name behind the formula.

Check out my complete guide to lists to learn more on how referencing columns returns a list.

// Notice the [result] at the end of below code
= Json.Document( 
      Web.Contents( "https://pokeapi.co/api/v2/pokemon?limit=100&offset=0" ))[results]

You can remove this column reference again, but it’s good to remember the effect of referencing a column name. We will use this later in this exercise.

Create Function for API Call

For the API calls, you will need to transform the code into a function. List.Generate can then call that function multiple times.

To do that, start with the previous code. When you open the advanced editor, the code generated by the From Web operation looks like this:

let
  Source = 
    Json.Document(
      Web.Contents(
        "https://pokeapi.co/api/v2/pokemon?limit=100&offset=0"
     )
   )
in
    Source

You want to use this code and apply it to a set of offsets. Since each request will have a different offset, you need a variable here. And to do that, you can best use a function.

To turn this code into a function, you:

  • Open the advanced editor and add the required variable(s). In this case, you need a variable for offset. To do that, input the name between parenthesis and end the first line with =>.
  • Next, you want to replace the offset value with the variable name. As Power Query expects a text value, transform the variable to text using Number.ToText.
(offset) =>
let
   Source = 
     Json.Document(
       Web.Contents(
         "https://pokeapi.co/api/v2/pokemon?limit=100&offset=" 
            & Number.ToText( offset )
      ) 
    )
in
    Source

This creates your new function. Rename the function to GetPokemon.

Do-While Loop

With the API function prepared, all that’s left is to use the List.Generate function to retrieve the data. The function needs to add increments of 100 to the offset variable of the formula.

Let’s first see a basic example of generating a list of values. List.Generate can create a series of values that increase by 100 in each step by writing:

= List.Generate(   () => [ Offset = 0 ], 
                   each [Offset] <= 200,                
                   each [ Offset = [Offset] + 100 ] )
// Returns { [Offset = 0], [Offset = 100], [Offset = 200] }

We are making a do-while loop that runs as long as the generated value is less than or equal to 200. The loop starts with the value 0.

Tip

If this went to quickly, you can read more examples on List.Generate in Power Query.

Return Specific Records

The List.Generate function does not only take scalar values as input but can also take structured values. Structured values could be values like lists, tables and records.

Your requirement is to create the list of offset values but also to return the values from the API call. The easiest way to do that is to return two columns in a record. You can transform the previous statement into a list of records by writing:

= List.Generate( () => [ Offset = 0 ],  
                 each [Offset] <= 200,                 
                 each [ Offset = [Offset] + 100 ] )
// Returns { [Offset = 0], [Offset = 100], [Offset = 200] }

This example showed a record with a single value, but you can add another value to it by writing:

= List.Generate( () => [ Offset = 0, Pokemon = "a" ],
                 each [Offset] <= 200  , 
                 each [ Offset = [Offset] + 100, Pokemon = [Pokemon] ] )
// Returns the following record
// { [Offset = 0, Pokemon = "a"], [Offset = 100, Pokemon = "a"], 
//   [Offset = 200, Pokemon = "a"] }

If you then only want to return the list of values from the [Pokemon] column in the record, you can write:

= List.Generate( ()  => [Offset = 0, Pokemon = "a" ], 
                 each [Offset] <= 200  ,  
                 each [ Offset = [Offset] + 100, Pokemon = [Pokemon] ], 
                 each [Pokemon] )
// Returns { [Pokemon = "a"], Pokemon = "a"], [Pokemon = "a"] }

The benefit of that is that you can incorporate both variables in your calculations, but only return one for the result.

You now know how to build up a List.Generate formula that takes multiple variables as an argument and returns a single variable. With that, you have all the building blocks for the formula that makes multiple API calls.

Multiple API Calls

Your next step is to incorporate the GetPokemon function into the List.Generate formula. You can start with the previous code and make the following adjustments:

  • Instead of returning a value for the Pokemon variable, enter the GetPokemon function. Provide the function with the value 0. This is the offset value that is used in the first request.
  • The third argument indicates that the Pokemon variable should take the offset value and apply it to the GetPokemon function.
= List.Generate( ()  =>  [ Offset = 100, Pokemon = GetPokemon(0) ], 
                 each [Offset] <= 200,  
                 each [ Pokemon = GetPokemon( [Offset] ), Offset = [Offset] + 100 ],  
                 each [Pokemon] )

Now, instead of returning the entire record on line 6, you can return the list values by referencing the column name results.

= List.Generate( ()  =>  [ Offset = 100, Pokemon = GetPokemon(0) ], 
                 each [Offset] <= 200,  
                 each [ Pokemon = GetPokemon( [Offset] ), Offset =  [Offset] + 100 ],  
                 each [Pokemon][results] )

So far, we hardcoded the amount of times the function was called. Since you want to make API calls until reaching the end of the database, you can check whether the List value is empty. Once there are no more records, Power Query will return an empty list.

At that point, List.Generate should stop making the calls. You can use the List.IsEmpty function to give those instructions.

= List.Generate( ()  =>  [ Offset = 100, Pokemon = GetPokemon(0) ], 
                 each not ( List.IsEmpty( [Pokemon][results] ) ) ,  
                 each [ Pokemon = GetPokemon( [Offset] ), Offset =  [Offset] + 100 ],  
                 each [Pokemon][results] )

With the above, you called a function multiple times in List.Generate and looped through all records until reaching the end of the dataset.

List.Generate calling a function

The result is a List containing Lists with Records. What’s left is to dive into the data and return the pokemon in the database.

  • In the Transform tab, select To Table to transform the list into a table.
  • In that table, click on the two opposite-facing arrows and select Expand to new Rows
  • Click those arrows once more and select only the name column.
4. List of pokemon from the API

Alternative Solution

The previous example checked whether the retrieved list had any values in it. An alternative is to use the next column in the record. When the next record does not contain any values, the column returns null.

= List.Generate( () => [ Offset = 100, Pokemon = GetPokemon( 0 ) ], 
                  each [Pokemon][next] <> null,  
                  each [ Pokemon = GetPokemon( [Offset] ), Offset = [Offset] + 100 ],  
                  each [Pokemon][results] )

Conclusion

And that wraps everything up! We’ve covered everything you need to know about how to use the List.Generate function to make API calls in Power Query. From making an API call, to creating a function that returns the relevant fields, you should now have a solid understanding of how to use this powerful function to access API data.

Did you come up with a different solution? Or did I miss out on something important? Let me know in the comments. And if List.Generate still has secrets for you, make sure to check out more examples in my foundational post on using List.Generate in Power Query.

Happy Querying!

Share this post:
  1. Great article,
    unfortunately the Alternative Solution won’t work correctly, specifically it will not get the last chunk of data. This has to do with wired condition checking algorithm in List.Generate, which drives me crazy. Its kinda checking the condition for the current and next iteration simultaneously. You can check it yourself, it makes only 13 api calls, instead of 14, which is correct sinse there are 1302 total records.

    let
    GetPokemon = (offset) => Json.Document(Web.Contents(“https://pokeapi.co/api/v2/pokemon?limit=100&offset=”& Number.ToText( offset ))),
    res = List.Generate( () => [ Offset = 100, Pokemon = GetPokemon( 0 ) ],
    each [Pokemon][next] null,
    each [ Pokemon = GetPokemon( [Offset] ), Offset = [Offset] + 100 ] )
    in
    res

    Reply
  2. Hi Rick,
    First of all thanks for the great article and youtube video. It helped a lot.
    But when trying to implement this I’m getting error for both the options which check end of iteration:
    Option 1: each [Pokemon][next] null,
    Option 2: each not ( List.IsEmpty( [Pokemon][results] ) ) ,
    Has this changed in latest version or does it depend on API we are using.
    I tried to handle this with total record count like below:
    Option 3: each [offset] <= [Pokemon][totalCount]
    But it is not executing last call to the API.
    Any solution on this.
    Thanks in advance.
    Abhay

    Reply
  3. You actually make it seem so easy with your presentation but I find this matter to be really something that I think I would never understand.
    It seems too complex and extremely broad for me. I’m looking forward
    for your next post, I will try to get the hang of it!

    Reply
  4. When someone writes an piece of writing he/she keeps the image of a
    user in his/her mind that how a user can know
    it. Thus that’s why this piece of writing is amazing.
    Thanks!

    Reply
  5. Everything is very open with a precise explanation of the issues.
    It was truly informative. Your site is very useful.

    Thank you for sharing!

    Reply
  6. I have read so mɑny poѕts on the topic of the blogɡer lovers except this
    post is truly а good ⲣaragraph, keep it up.

    Reply
  7. Hi Rick

    I am pulling data from an API where the limit is 100, but the offset number is generated and you have to use that in the next call. For example when i run the first call the offset is = 668052460, then the next call should contain this number in the offset. That will then generate a new number.

    Is there anyway of looking at the number instead?

    Documenation: “Offset offset=
    Used in the request URL Used to page through the results. If there are more records in your account than the limit= parameter, you will need to use the offset returned in the first request to get the next set of results.”

    Reply
  8. Hi,
    Is it possible to create a new call based on last id from first call? For example, fitst call returns 250 records and we grab last id from extracted records to use this value for making next call with parameter since_id=(last id from previous call). And iterate such a way until there are less than 250 records returned from a call.

    Reply
  9. Hi Rick, so glad to find your blog!

    I’m able to pull data, but I’m trying send to an API- specifically a GraphQL mutation. It works, sortof, but with some bizarre behaviour I can’t figure out- it duplicates the record on the endpoint in most- but not all cases. The M:

    let
      Source = Web.Contents(
        "https://api.monday.com/v2", 
        [
          Headers = [
            #"Method"        = "POST", 
            #"Content-Type"  = "application/json", 
            #"Authorization" = APIKey
          ], 
          Content = Text.ToBinary(
            "{""query"": ""mutation { create_item (board_id:#####,
    group_id:#####, item_name:wazoo) { id } }""}"
          )
        ]
      ), 
      #"JSON" = Json.Document(Source)
    in
      #"JSON"

    This works but fires twice.
    FYI I have un-checked all the refresh boxes in Properties.

    The #JSON line is from a version where I’m actually bringing data into PQ. I don’t really need it since I’m not parsing anything. So I took it away, and just returned Source. This seems to fire the mutation only once (yay!) but returns an error, because Excel wants something to do. The M:

    let
      Source = Web.Contents(
        "https://api.monday.com/v2", 
        [
          Headers = [
            #"Method"        = "POST", 
            #"Content-Type"  = "application/json", 
            #"Authorization" = APIKey
          ], 
          Content = Text.ToBinary(
            "{""query"": ""mutation { create_item (board_id:2298061107, 
              group_id:new_group51930, item_name:wazoo) { id } }""}"
          )
        ]
      )
    in
      Source

    Do you know how I could approach this differently? Thanks!

    Reply
  10. Hi Rick,

    So happy to see this function broken down and explained so well! I first had to use it a few years ago, and it was horrific to work out what to do, despite Microsoft’s documentation – this would have helped considerably!

    I noticed a couple of things when I tried to implement this myself – I was getting a repeated first 100 records 🙁 . Did you see this with your GetPokemon query? I’ve since implemented an “if then else” check (not shown), which bypassed the issue, but wondering if I’ve done something wrong?

    GetBulkData = ((environmentName as text, endpoint as text, 
           offsetAmount as number, limitAmount as number) as record =>
    let
    Source = Json.Document(Web.Contents("https://myApiAddress",
    [
      RelativePath = "api/" & "?offset=" & Number.ToText(offsetAmount) 
        & "&limit=" & Number.ToText(limitAmount),
      Headers = [ Authorization = “Bearer ” & validAuthToken ]
    ]))
    in
      Source),
    data = List.Generate(
    () =>
    [offset = 0, data = GetBulkData(0, 100)],
    each [data][nextUrl] null),
    each [data = GetBulkData([offset], 100),
    offset = [offset] + 100],
    each [data][jobs]
    )

    Best wishes,

    Ed HP

    (PS you’ve written the query as Pokemon = GetGet( [Offset] ) in the final example)

    Reply
    • I think the duplication of the first 100 can be resolved by changing the second “with” to increment Offset and then reference this new value. Ie.

      each [ Offset =  [Offset] + 100,  Pokemon = GetPokemon( Offset )  ]
      Reply
    • Thanks for mentioning it Ed. The first call to the API indeed was duplicated. I have changed the first Offset to 100. It’s now reflected correctly in the article. Hope that helps!

      Reply
  11. Nice blog about less explored area of Power Query, thanks for sharing the wealth. Please keep it coming. Couple of questions:
    Can PQ call be authenticated, and
    Can it be POST if I want to push my data into the source?

    Reply
    • Thanks Vlad, appreciate you taking the effort to comment. It’s a fun area to delve into!

      About your question. I’m not completely sure a call can be authenticated. I think if it requests credentials you can provide credentials.

      Yet my experience with API call is as little as this post. This was a first time. If I had an API we could test this with that would be fun.

      Best,
      Rick

      Reply
    • Hi Vlad,

      Yes, a PQ call can be authenticated. Depending on your solution you want to implement, adding a Headers property may suffice, or if you want to have automated data refresh then you’ll need to make a custom connector and handle authentication in there. Here’s the amended code from Rick’s example:

      (offset) =>
      let
        Source = Json.Document(
          Web.Contents("https://pokeapi.co/api/v2/pokemon
                        ?limit=100&offset=" & Number.ToText( offset ),
          [
            Headers = [Authorization = "Bearer " & myAuthToken]
          ])
      )
      in
        Source

      Here’s Microsoft’s own documentation on the matter: https://docs.microsoft.com/en-us/power-query/handlingauthentication

      Reply

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.