Documenting with PowerShell: Handling IT-Glue API security and rate limiting.

I’ve been blogging a whole lot about documentation lately; I truly believe all automated documentation is better than just having people enter data manually. My company uses IT-Glue as a documentation system. IT-Glue is a very cool system but has some huge API limitations. For example; You’re allowed to make 10 requests per second and 10,000 requests per day. These limitations can get pretty bad if you manage a lot of workstations or servers that upload data at the same time.

After my previous blogs the comment I’ve received most was worries about the API key. If they key gets stolen you’re giving away the keys to the castle. The API has no limitations and with a leaked key all your documentation could be download. I’ve been discussing this issue with IT-Glue for some time but haven’t gotten a real solution yet. This has forced me to look for a solution myself. I gave myself some requirements for the solution.

  • The solution needed to be simple and accessible for everyone.
  • The solution needed to have multiple levels of authentication; an API key, IP whitelisting, and organization whitelisting.
  • The solution needed to block requests for all passwords/files/etc for all organisations.
  • The solution needed to allow some form of handling of the API rate limiting, e.g. repeating a request if it was rate limited.
  • The solution needed to be able to used, without adapting any scripts (except URLs and API codes.)

So after some research I decided to use an Azure Function for this. I’ve blogged about Azure Functions before, but the main reason is that running this function in the consumption model will cost us nothing (or next to nothing if you are an extremely heavy user.)


This time we will not use the Azure Function to only run a script but act as a “middleware” for the IT-Glue API. Follow this guide to set up your Azure function App. The only difference is that we select “PowerShell” as our runtime language. Do not continue at “Create an HTTP triggered function” as we’re going to be inserting our own function.

When the Function App has been deployed click on your Function’s name and then on “platform features”. You should be presented with the following screen

In this screen click on “Configuration” – We’re going to be adding some configuration options here that are used in our scripts. Add the three following items:

  • AzAPIKey: This will be the new API key you will enter on all your scripts that will upload data to IT-Glue. Generate a password for this or enter one of choice.
  • ITGlueURI: This is the current IT-Glue API url you use, most likely or
  • ITGlueAPIKey: Your current API key. This is the only location that this API key will be used from now on.

After this you can return to the overview page and click the + symbol next to the “Functions”, Choose the “HTTP trigger” option. Name the HTTP trigger “AzGlueForwarder” and choose the Anonymous Authorisation level. This is because we are going to take care of authentication on the script level and not at the Azure Function level. After creating the function you’ll be presented with a script page. Paste the following script:

using namespace System.Net
param($Request, $TriggerMetadata)
#Check if AZapiKey is correct
if ($request.Headers.'x-api-key' -eq $ENV:AzAPIKey) {
    #Comparing the client IP to the Organization list, and checking if it exists.
    $ClientIP = ($request.headers.'X-Forwarded-For' -split ':')[0]
    $CompareList = import-csv "AzGlueForwarder\OrgList.csv" -delimiter ","
    $AllowedOrgs = $comparelist | where-object { $_.ip -eq $ClientIP }
    if (!$AllowedOrgs) { 
        Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
                headers    = @{'content-type' = 'application\json' }
                StatusCode = [httpstatuscode]::OK
                Body       = @{"Error" = "401 - No match found in allowed list" } | convertto-json
        exit 1
    #Sending request to ITGlue
    $resource = $request.url -replace "https://$($ENV:WEBSITE_HOSTNAME)/API/", ""
    #Replace x-api-key with actual key
    $ITGHeaders = @{
        "x-api-key" = $ENV:ITGlueAPIKey
    $Method = $($Request.method)
    $ITGBody = $($Request.body)
    #write-host ($AllowedOrgs | out-string)
    $SuccessfullQuery = $false
    $attempt = 3
    while ($attempt -gt 0 -and -not $SuccessfullQuery) {
        try {
            $ITGlueRequest = Invoke-RestMethod -Method $Method -ContentType "application/vnd.api+json" -Uri "$($ENV:ITGlueURI)/$resource" -Body $ITGBody -Headers $ITGHeaders
            $SuccessfullQuery = $true
        catch {
            $ITGlueRequest = @{'Errorcode' = $_.Exception.Response.StatusCode.value__ }
            $rand = get-random -Minimum 0 -Maximum 10
            start-sleep $rand
            if ($attempt -eq 0) { $ITGlueRequest = @{'Errorcode' = "Error code $($_.Exception.Response.StatusCode.value__) - Made 3 attempts and upload failed. $($_.Exception.Message) / Resource was $($ENV:ITGlueURI)/$resource" } }
    #Checking if we can strip the data that does not belong to this client. 
    #Important so passwords/items can only be retrieved belonging to this organisation.
    #Can't do it for all requests, such as get-organisation, but for senstive data it works perfectly. :)
    if ($($'organization-id')) {
        write-host ($AllowedOrgs.ITGlueOrgID)
        $ = $ | where-object { $_.attributes.'organization-id' -in $($AllowedOrgs.ITGlueOrgID) }    
    #Sending the final object back to the client.
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
            headers    = @{'content-type' = 'application\json' }
            StatusCode = [httpstatuscode]::OK
            Body       = $ITGlueRequest
else {
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
            headers    = @{'content-type' = 'application\json' }
            StatusCode = [httpstatuscode]::OK
            Body       = @{"Error" = "401 - No API Key entered or API key incorrect." } | convertto-json

Save the script and use the right-hand menu to add a file to the function. Call this file “OrgList.csv”. This is the database that will be used to check which IP’s are allowed to upload data, and for which organisations they can retrieve data.


Next click on “Integrate” and select the allowed methods, in our case we want all methods selected for the IT-Glue API. Replace the “Route template” with “{*path}”.

Click on AzGlueForwarder once more and press “Get Function URL” and copy this URL up to the {PATH} part. This will be the URL you will put in place of the API endpoint variable in your scripts. e.g. “”.

And that’s it! A small recap:

  • Create the Azure Function
  • Add the environment variables AzAPIKey ITGlueBaseURI,ITGlueAPIKey.
  • The function URL will be your new IT-Glue API url to put in your scripts
  • The AzAPIKey is the key to put in your script.
  • The IT-Glue API key will only remain at the Azure Function side.
  • The OrgList.CSV file should contain your client’s their IP’s and allowed organisation.
  • your API requests can only be used for the organisations defined in OrgList.CSV.
  • When an API call fails, the script will try again 3 times, each with a random wait between 1 and 10 seconds to prevent rate limiting from getting in the way.

It’s a fairly simple but clean solution while I try to work with our friends at IT-Glue to increase the API limitations. It also helps on the security side as no one will be able to just download your entire database.

That’s it for today. As always, Happy PowerShelling.


  1. Mat January 12, 2020 at 10:47 am

    The great example to use Azure Function and PowerShell. Thanks!

  2. Andy February 10, 2020 at 6:39 pm

    This looks great and solves one of my issues preventing me from rolling out ITGlue to all clients.
    Now if only we had a flexible asset with the external ip listed for each client that I could easily export in the global settings to create my csv!

    1. Kelvin Tegelaar February 10, 2020 at 6:43 pm

      I know some friends of mine have already modified it to pick up the list directly from IT-Glue ๐Ÿ™‚ anyway, glad it helps!

  3. Pingback: Documenting with Powershell: Documenting Hyper-V settings - CyberDrain

  4. John Gard February 28, 2020 at 11:52 pm

    Thank you so much for this! We’re fairly new to ITGlue and glad we came across your post. However, I’m currently running into the following error message. Any advise on where we messed up?

    PS C:\Windows\system32> C:\TEMP\ITGlueUpload.ps1
    Checking if Flexible Asset exists in IT-Glue.
    Start documentation process.
    Documenting to IT-Glue
    Creating Hyper-v into IT-Glue organisation 2456789
    New-ITGlueFlexibleAssets : {“errors”:[{“status”:404,”title”:”Not found”,”detail”:”Record not found”,”source”:{“pointer”:”id”}}]}
    At C:\TEMP\ITGlueUpload.ps1:129 char:5
    + New-ITGlueFlexibleAssets -data $FlexAssetBody
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,New-ITGlueFlexibleAssets

    1. Kelvin Tegelaar February 29, 2020 at 12:19 am

      Most likely you’ve made a small mistake in the URL your using. It should be just “”. I’ve also updated the code block as I saw some parts got stripped out by WordPress. it might help reuploading the code.

  5. Dustin March 2, 2020 at 1:50 am

    I am not sure what I am doing wrong, but am getting an internal error 500

    Get-ITGlueFlexibleAssetTypes : The remote server returned an error: (500)
    Internal Server Error.
    At line:24 char:14

    I have made sure the OrgList has the correct IP and company ID.

    Monitor tab shows the first log error
    Script compilation failed.

    1. Dustin March 2, 2020 at 8:32 pm

      I actually figured out one part, I created it as .NET rather than a PowerShell stack (oops). I have recreated and it looks like it accessing IT Glue, but any attempt to create a flexible asset fails and I get a 403 error, the logs on Azure look fine and are showing successful, so it must be between Azure and ITG (I cannot find logs in ITG for the API commands)
      If I put the URL and API key directly into the script, circumventing the Azure Function, it does work so I know my authentication and ITG tenancy is working correctly.

      1. Logan March 6, 2020 at 8:15 pm

        I do am experiencing the same exact issue as Dustin. I have also deleted and re-created the function without success. Scripts using same ITGlue API Key, OrgID, and the actual ITGlue API Endpoint work without issue.

      2. Kelvin Tegelaar March 8, 2020 at 3:04 pm

        Glad you were able to fix the first part. Setting it up as a PowerShell function is key yes.

        Have you changed the URLs and API key inside of the properties of the Azure Function? I’ve seen people forgot the API URL or set it to the EU version.

        Could you show how the 403 errors looks? e.g. “”Error code 403 – Made 3 attempts and upload failed.Access denied”?

        1. Logan March 20, 2020 at 1:32 am

          Sorry for the delayed response!

          Here is my exact output:
          Creating new flexible asset
          Error code 403 – Made 3 attempts and upload failed. Response status code does not indicate success: 403 (Forbidden).
          Error code 403 – Made 3 attempts and upload failed. Response status code does not indicate success: 403 (Forbidden).

          Yes, Azure side matches up with the correct variables (AzAPIKey, ITGlueURI, ITGlueAPIKey) under Home > All Resources > APPNAME > Platform Features > Configuration, then “Application settings” tab is where I added those variables and supplied the values for each.

          I can get you full screenshots if you prefer, just let me know how you want to receive those. Also, still successful when replacing my variable values with default ITGlue API URL and my API Key.

          1. John Dorman May 7, 2020 at 12:16 am

            Has anyone been able to resolve the 403 error? I’ve re-created the Azure Function and API Keys and still get the same error every time. Like others if I upload directly the scripts work but not using the Azure function

          2. Kelvin Tegelaar May 7, 2020 at 10:24 am

            I haven’t been playing with the Azure Function lately but here it’s working as expected. I’m thinking it has to do with the API key, I’ll try to simulate the issue by creating a new application and API key soon. ๐Ÿ™‚

          3. John Dorman May 7, 2020 at 2:54 pm

            In playing around with various code integrations. It’s definitely something on the Azure side. The monitoring on Azure shows a connection.

            If I try to query anything within ITGlue such as,


            I get the same 403 error codes. It’s as if the Forwarder is not correctly relaying the true API Key to IT Glue.

          4. Kelvin Tegelaar May 19, 2020 at 12:43 pm

            Fixed! ๐Ÿ™‚ It seems like the Azure Function was adding a trailing slash. Please recopy the script. If you have any more requests, I’m open to changes on my new github: feel free to create issues there! ๐Ÿ™‚

          5. Kelvin Tegelaar May 19, 2020 at 12:43 pm

            Fixed! ๐Ÿ™‚ It seems like the Azure Function was adding a trailing slash. Please recopy the script. If you have any more requests, I’m open to changes on my new github: feel free to create issues there! ๐Ÿ™‚

      3. Kelvin Tegelaar May 19, 2020 at 12:43 pm

        Fixed! ๐Ÿ™‚ It seems like the Azure Function was adding a trailing slash. Please recopy the script. If you have any more requests, I’m open to changes on my new github: feel free to create issues there! ๐Ÿ™‚

  6. Michael McCool May 19, 2020 at 9:09 pm

    I had a call with ITGlue today, and asked specifically about the “Rate Exceeded” error code. They said it is 429. So only this specific error code needs to be retried. All others can be passed back without retrying the command.

    As to the rest of the various error codes, the are all listed in each of the specific API sections. As an example, this one shows a handful under the Errors section. Other commands have slightly different lists depending on what is appropriate for each command, but the numbers are at least consistent across the board.

    1. Kelvin Tegelaar May 19, 2020 at 9:29 pm

      That’s great. We’ve seen different error codes in the past, but we do only need retries on 429. I’ll check if I can update the blog somewhere this week. ๐Ÿ™‚

  7. Heine May 28, 2020 at 10:34 am

    Hi Kelvin,

    Great work, we have two issues. One is the above 403 forbidden message with the below result from powershell:

    PS C:\Users\x\Desktop\ITG_AutoDocWorkstation_UniProfs.ps1
    Checking if Flexible Asset exists in IT-Glue.
    Does not exist, creating new.

    Starting documentation process.
    Getting update history.
    Getting User Profiles.
    Getting Installed applications.
    Checking WAN IP
    Getting Installed applications in last 24 hours for events list
    Getting KBs in last 24 hours for events list
    Getting user logon/logoff events of last 24 hours.
    Uploading to IT-Glue.
    Documenting to IT-Glue
    Creating Device Asset Log into IT-Glue organisation xxxxx
    Error code 403 – Made 3 attempts and upload failed. Response status code does not indicate success: 403 (Forbidden). / Resource was
    Error code 403 – Made 3 attempts and upload failed. Response status code does not indicate success: 403 (Forbidden). / Resource was

    The other one is a DateTime issue with the Installed applications:

    MethodInvocationException: C:\Users\x\Desktop\ITG_AutoDocWorkstation_UniProfs.ps1:133:5
    Line |
    133 | $Application.InstallDate = [datetime]::parseexact($Application.In โ€ฆ
    | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    | Exception calling “ParseExact” with “3” argument(s): “String ‘5/23/2020’ was not recognized as a valid DateTime.”

    It must have something to do with this that i’ve currently disabled from running:

    #$InstalledSoftware = (Get-ChildItem “HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall” | Get-ItemProperty) + ($software += Get-ChildItem “HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall” | Get-ItemProperty) | Select-Object Displayname, Publisher, Displayversion, InstallLocation, InstallDate
    #$installedSoftware = foreach ($Application in $installedSoftware) {
    # if ($null -eq $application.InstallLocation) { continue }
    # if ($null -eq $Application.InstallDate) { $application.installdate = (get-item $application.InstallLocation -ErrorAction SilentlyContinue).CreationTime.ToString(‘yyyyMMdd’) }
    # $Application.InstallDate = [datetime]::parseexact($Application.InstallDate, ‘yyyyMMdd’, $null).ToString(‘yyyy-MM-dd HH:mm’)
    # $application

    Region is NL.

    Any ideas?

    1. Kelvin Tegelaar May 28, 2020 at 10:39 am

      Hi Heine,

      1.) In the configuration of the AzGlue forwarder, You’ve probably entered the $ENV:ITGlueURI as “” Please remove that trailing slash and make that into “” – I’ll see if I can add some validation in that later.

      2.) Sometimes the date/time does not parse well and it can’t find it. It’s an ignorable error, if you don’t want the error to show up, you could remove the entire installdate from the selection.

      What RMM are you using? That also really depends on how to ignore the error. ๐Ÿ™‚

      1. Heine May 28, 2020 at 10:58 am

        Hi Kelvin,

        Thanks for the fast reply. I fixed the first one, but not by changing the ITGlue URL, that was fine. I changed it in the AzGlueForwarder function by deleting the slash between the ENV and the Resource:

        “application/vnd.api+json” -Uri “$($ENV:ITGlueURI)/$resource” -Body $ITGBody –

        My version:
        “application/vnd.api+json” -Uri “$($ENV:ITGlueURI)$resource” -Body $ITGBody –

        That worked.

        Thanks for the second one, i’m currently still running it on my Visual Studio Code from my PC, but we want to run it on Kaseya’s VSA.

        1. John Dorman May 28, 2020 at 5:52 pm

          Thanks Heine!!! This fixed the issue I was having as well.

          1. Heine May 28, 2020 at 6:54 pm

            Great! Thanks!

          2. Kelvin Tegelaar May 28, 2020 at 6:59 pm

            I’ve updated the AzGlue script too, by stripping away the extra slash at the request side, so if you want you can use the latest version. ๐Ÿ™‚

  8. Brian N. June 5, 2020 at 4:14 am

    i cannot get this to work to set a password. I just reports back a 404:

    Set-ITGluePasswords : The remote server returned an error: (404) Not Found.

    I am able to query passwords just fine, just cannot post a password. I have not tried to post to other asset types.

  9. Stephen Moody July 24, 2020 at 8:23 am

    Hi Kelvin,
    This is great! Trying to use the latest version. When I query /configurations I only get meta returned, and no data. I’ve tried adjusting some of the parameters in the whitelisted-endpoints.yml but no luck so far. Other endpoints all return data. Any idea what I might be missing?

    1. Kelvin Tegelaar July 24, 2020 at 9:13 am

      You’ll have to make sure you have filled in the CSV file with the correct organisation and IP address, and whitelist the endpoints you are trying to reach. Most people forget about the CSV ๐Ÿ™‚

  10. Bryan Schulz July 27, 2020 at 3:28 pm


    I am having a issue where when I run the script it would give the following error:
    After that we re-ran the script and got the following error:
    The term โ€˜enter path or command to execute e.g.: C:\Users\NAME\COMPANY\Coding โ€“ General\AZGlue/src/foo.ps1 or Invoke-Pesterโ€™ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
    + CategoryInfo : ObjectNotFound: (enter path or cโ€ฆr Invoke-Pester:String) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : CommandNotFoundException

    Cannot get past step 5 in the README.txt

    After editing the .Json file to this:
    “version”: “0.2.0”,
    “configurations”: [
    “name”: “Attach to PowerShell Functions”,
    “type”: “PowerShell”,
    “request”: “attach”,
    “customPipeName”: “AzureFunctionsPSWorker”,
    “runspaceId”: 1,
    “preLaunchTask”: “func: host start”
    it would not run the script for my boss

    I re ran the script by clicking F5, it ran but give me a error stating:

    ” Attatching to a process with CustomPipeName is only available with PowerShell 6.2 and higher”

    When I attempted to install the latest version the VSC kept erroring out and not running.

    Any ideas or tips?

  11. Jeremy July 29, 2020 at 6:00 pm

    Thank you for the blog, but I’m having trouble following along with the steps as it appears you are on a completely different webpage than I am with your function. I have a function created after logging into but starting at “When the Function App has been deployed click on your Functionโ€™s name and then on โ€œplatform featuresโ€. You should be presented with the following screen” I don’t have that tab at all, although I found the configuration and added the items you suggested. Next was “Save the script and use the right-hand menu to add a file to the function. ” and there’s nothing on the right hand at all in the Azure pages I have looked through, and then the integrate section I am completely lost. I was able to create an upload the .csv through some Kudu “zip push deploy” option, but no idea if that is correct. If there’s another site to manage function apps from rather than the default Azure portal then I suppose that makes sense. Any help would be appreciated!

    1. Chris J August 24, 2020 at 10:35 pm

      These instructions don’t match the new UI. If you go to the Overview of your function there is an option to “Switch to classic experience”. That will make these instructions make more sense.

      Most of this works in the new UI but I could find no way to upload the OrgList csv with the new UI anywhere.

      1. Kelvin Tegelaar August 25, 2020 at 8:53 am

        Correct! the big UI overhaul has caused some changes. ๐Ÿ™‚

    2. Lawrence Chu October 30, 2020 at 11:22 pm

      In the new experience, it looks to me like you go to “Code + Test” on the left-hand menu, then you click “upload” in the right pane. You’ll be able to choose from the files in your AzGlueForwarder menu from the drop-down menu right above the file contents.

  12. Pingback: Automating with PowerShell: Deploying Azure Functions - CyberDrain

  13. Lawrence Chu October 31, 2020 at 12:06 am

    Hey! Finally getting around to actually trying to implement some of these things and I figured I’d start here.

    Would I be correct in assuming the CSV works on an n-to-n mapping? One host to multiple orgs (e.g. my MSP should be able to manage all the tenants) and multiple hosts for one org (e.g. both my MSP and the tenant itself should be able to modify the tenant’s entries), e.g.


  14. Brian April 1, 2021 at 7:54 pm

    Hi Kelvin,
    Your powershell scripts for auto documenting into IT Glue have been a huge help. Ive also been following along with this Azure Function and your Git Hub. I’ve successfully used it with Azure for AD documenting and am working on getting it setup with DHCP documenting. The new security features implemented are great and I had to add some additional mappings in the .yml file for the DHCP script to run, but I cant find what i need to do to allow the script to see the existing flex-asset in it-glue. Every time I run the DHCP script through Azure it creates a new one, try it without Azure and it updates the existing flex asset. Any insight would be great, thank you!

Leave a comment

Your email address will not be published. Required fields are marked *

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