At Omnissa One, I took attendees on an API adventure in session TSH452 — a hands-on tour of the Omnissa Horizon REST API. The API lives past the “recommended settings,” past the “don’t worry about that warning,” and way past the part of the console everyone pretends to understand. It’s a door very few have choose to open and step through.
Instead, many admins keep doing things the hard way, battling desktop operations one at a time and fighting their evil nemesis: time. That ended for my session attendees. They walked out with a new set of API tools in their toolbelt, the kind that make your boss think it’s magic and your coworkers think you’re a superhero.
For those who were there — or wished they were — I’m sharing all the knowledge from my session right here in this post.

From SOAP to REST: The Horizon View API Is Being Deprecated
Astute readers may notice the session was originally supposed to cover the Virtual Language System Interface (VLSI) API, the SOAP-based interface. And it was! That’s what I’d used in the past whenever I wanted to work programmatically with an Omnissa Horizon environment.
Then I went to the Omnissa Developer site to start building my presentation… and guess what? Omnissa KB 6000139 states:
“…deprecation of the Horizon View APIs in the Q2 Release of 2026 (earlier planned for the Q4 Release of 2025) and its removal in upcoming future releases… The Horizon Q2 2026 Release will be the last version to include the SOAP-based Horizon View APIs. The newer releases after Q2 2026 will only contain the Horizon REST APIs.”
Translation: let’s make sure this presentation stays useful.
Your Built-In API Documentation: Swagger
So, running to the nearest phone booth and swapping our superhero capes, let’s talk about the Omnissa Horizon Server REST API. There’s a great section covering it on the Omnissa Developer site, but the better news is that your connection server ships with a built-in Swagger instance that documents the API for your specific environment. You can reach it at:
https://[connection-server]/rest/swagger-ui/index.html
If you haven’t used Swagger before, it’s a powerful API documentation tool that tells you just about everything you need to make a given API call: the structure of the request, the possible return codes, and what the response content looks like.
We’ll use PowerShell for these examples because we’re just getting started. If you wanted to build a full system, you’d reach for something like Python. Luckily, most of this code ports over easily, because it’s all just API calls.
Step 1: Authenticate and Get a Bearer Token
The first thing we need is a connection with the API: what REST folks call getting a bearer token. The idea is simple: authenticate once, then fire off a bunch of API calls by presenting the token instead of re-authenticating every time. Eventually the token times out and you renew or request a new one.
Here’s how you get that bearer token and build a reusable header with it:
# Bypass cert validation (PS 5.1 & 7 compatible fallback)
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$BaseUrl = "https://[connection server]/rest"
# Construct credentials body for initial login
$Body = @{
"username" = "api_admin"
"password" = "SecretPassword"
"domain" = "YOURDOMAIN"
} | ConvertTo-Json
# Request the login token
$LoginResponse = Invoke-RestMethod -Uri "$BaseUrl/login" -Method Post -Body $Body -ContentType "application/json"
# Capture the bearer token (Horizon returns access_token)
$BearerToken = $LoginResponse.access_token
# Define standard headers for future calls
$Headers = @{
"Authorization" = "Bearer $BearerToken"
"Accept" = "application/json"
}
A few things worth noting in this block:
- We start by bypassing certificate validation because my lab uses self-signed certs. If you’re working in production, set this to
$false. - I create a
$BaseUrlvariable so I only have to change one location if the base REST URL ever changes. - The login body carries our credentials to the connection server. I strongly recommend creating a dedicated API user for these tasks; it makes auditing much easier. Bonus: Swagger tells you which privileges each API call requires, so you can limit the account to only the permissions it needs.
- We convert the body to JSON, then we can make our first call.
Throughout the rest of this post we’ll use Invoke-RestMethod for every API call to the Horizon connection server. This first call appends the login path to the base URL, uses the POST method, and passes the body we just built. The response returns several items, but the only one we care about right now is access_token. Drop that token into the $Headers hashtable and we can make additional calls without re-authenticating.
Step 2: Anatomy of an Invoke-RestMethod Call
Let’s add another tool to the toolbelt by looking closer at how Invoke-RestMethod is structured. Each call needs:
-Uri– the path of the API you want to run-Method–Get,Post,Put, orDelete-Headers– your bearer token-Body– typically a JSON payload (when required)-ContentType–"application/json"
That’s everything a typical PowerShell API call needs, and the return value is a JSON response. The basic building block looks like this:
$Response = Invoke-RestMethod -Uri "$BaseUrl/$APIpath" -Method Get -Headers $Headers -Body $JsonPayload -ContentType "application/json"
Now that we understand the pattern, let’s do something practical.
Step 3: List Horizon Desktop Pools
Let’s find out which desktop pools exist in our environment. Heading back to Swagger, the endpoint is inventory/v1/desktop-pools, it uses GET, and no request body is required:
# API location
$DSPoolPath = "inventory/v1/desktop-pools"
# API GET call
$DSPoolList = Invoke-RestMethod -Uri "$BaseUrl/$DSPoolPath" -Method Get -Headers $Headers
This returns a listing of every desktop pool and all of their settings. That’s genuinely useful: it’s the foundation for a script that checks whether desktop pools have been added or removed over time. Think daily reports.
Step 4: Disable (and Re-enable) a Desktop Pool
Let’s take it a step further and disable a pool. It’s a two-step process: the disable action doesn’t accept pool names, so first we need the pool ID, then we disable it.
The first part is the longer one: get the pools, extract the ID (or IDs, since yes, you can do more than one at a time), and convert them into a JSON array:
# Get pool information
$DSPoolList = Invoke-RestMethod -Uri "$BaseUrl/$DSPoolPath" -Method Get -Headers $Headers
# Create a JSON array of desktop pool IDs
$DSPoolID = $DSPoolList.id
$DSPoolRawIDs = @($DSPoolID)
$DSPoolJsonIDs = ConvertTo-Json -InputObject $DSPoolRawIDs # PowerShell 5.1+ compatible
Let me pause on that last line. I only have a single desktop pool in this lab, which is why there’s no loop hunting for a specific pool. More importantly, the JSON body expects an array, and ConvertTo-Json treats a single-item array as a single object and drops the square brackets — which breaks the call. Using -InputObject preserves the brackets, and this approach stays backwards compatible with PowerShell 5.1.
Now that we have the pool ID, disabling it looks familiar: same structure as before, except it’s a POST that needs a -Body (the JSON array we just made):
# Disable the desktop pool
$DSPoolDisResult = Invoke-RestMethod -Uri "$BaseUrl/$DSPoolPath/action/disable" -Method Post -Headers $Headers -Body $DSPoolJsonIDs -ContentType "application/json"
The desktop pool should now be disabled. To re-enable it, run almost the same command and just change disable to enable in the URI.
Step 5: Modify a Desktop Pool (the Big One)
One last tool for the toolbelt: modifying a desktop pool. This one requires retrieving several different IDs and potentially a very long JSON body; it can easily run over 100 lines. Why? Think about everything you configure across all those screens when you create a desktop pool. All of that information is reflected in a pool update.
Here’s the first part: retrieving the information we need, then building the JSON body with the appropriate schema:
# Retrieve the access group
$AccessGroups = Invoke-RestMethod -Uri "$BaseUrl/config/v1/local-access-groups" -Method Get -Headers $Headers
# Retrieve the pool ID
$DSPoolList = Invoke-RestMethod -Uri "$BaseUrl/$DSPoolPath" -Method Get -Headers $Headers
$DSPoolID = $DSPoolList.id
$UpdateDSPoolPayload = @{
"access_group_id" = $AccessGroups.id
"allow_multiple_user_assignments" = $true
"display_name" = "API TESTING POOL"
"enabled" = $false
# ... the rest of the schema goes here
}
# Convert payload
$ConvertedDSPool = $UpdateDSPoolPayload | ConvertTo-Json -Depth 5
The access group and pool ID retrievals should look familiar by now. I cut the payload short because, frankly, I suspect you don’t want to scroll through nearly 100 lines of code to reach this paragraph. Even IT superheroes have their limits.
That last line adds something new: -Depth 5. The JSON body contains nested arrays, and ConvertTo-Json doesn’t fully traverse them by default, so we explicitly tell it to go five levels deep.
With the JSON body built, we can attempt the update:
try {
$DSPoolUpdate = Invoke-RestMethod -Uri "$BaseUrl/$DSPoolPath/$DSPoolID" -Method Put -Headers $Headers -Body $ConvertedDSPool -ContentType "application/json"
}
catch [System.Net.WebException] {
$Reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$ResponseText = $Reader.ReadToEnd()
Write-Error "HTTP Status: $($_.Exception.Response.StatusCode)"
Write-Error "Server Response: $ResponseText"
}
This is the first time I’ve used a try/catch block in this post, and there’s a good reason for it. The schema for this call includes a bunch of fields that may or may not apply to your desktop pool, and if they don’t apply, they’ll make the call fail. The catch block surfaces the server’s actual response so you can figure out what to add or remove. Here’s what that looks like in practice:
APIcall.ps1 : HTTP Status: The remote server returned an error: (400) Bad Request
+ .\OmnissaOneScripts_v4.ps1
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,OmnissaOneScripts_v4.ps1
Server Response: {"status":"BAD_REQUEST","timestamp":1789503512351,"errors":[{"error_key":"inventory.desktop-pool.provisioning_settings.automated.unset.error","error_message":"provisioning_settings must be set for automated desktop pools."}]}
The error_message at the end is the gold. In this case, I never configured provisioning_settings for my desktop pool, so I add it to the JSON payload and run the call again. I’ll let you iterate on your own payload for your environment.
Get the Slides and Scripts
Hopefully this adds a few new tools to your toolbelt, and helps you feel more comfortable walking through that door few dare to venture through.
I’ve posted the full slide deck from Omnissa One for your enjoyment and reference: link to slides. All the code from the session is also on my GitHub account for you to use.
Try things out, see what you can develop, and share what you build on developer.omnissa.com.












2 comments
Great job !. This is exactly what I need for now. for list desktop pool, anyway to add more info than the default output such as golden images/snapshots for those pool, max dv provisioned VDs etc? Thank you !
Author
I didn’t talk about versions of the API. There are 9 different versions available to choose from. What you are looking for starts showing up in version 7 I think, and version 9 has a full detail. So your URI would be /inventory/v9/desktop-pools. You would probably also want to include a -depth 5 when you convert it from JSON so that you get the full details.
The code would looks something like this:
#API location$DSpoolPath = "inventory/v9/desktop-pools"
#API Get call
$DSPoolList = Invoke-RestMethod -Uri "$BaseUrl/$DSpoolPath" -Method Get -Headers $Headers