Where this started
While working on a monitoring integration for a logistics client, I got an idea for a small helper script that would pull ticket counts from their ITSM API every 15 minutes and drop them into a dashboard. The whole thing ended up being about 40 lines of PowerShell, and the heavy lifting was a single cmdlet — PowerShell Invoke-RestMethod. I’ve since reused that pattern on probably a dozen engagements, so here’s the working version with the bits I learned the hard way.
This post covers the cmdlet itself, the common HTTP verbs against a REST endpoint, JSON payloads, headers and auth, and error handling. Explaining REST design or how to build your own API is outside the scope of this post — the vendor’s API docs are always the better source for that.
Notes before we start
- Examples were tested on PowerShell 7.4 against
https://jsonplaceholder.typicode.com, a free fake REST API that behaves like the real thing for GET/POST/PUT/DELETE. Invoke-RestMethodauto-parses JSON responses into PowerShell objects.Invoke-WebRequestgives you the raw response plus headers and status codes if you need them.- If you need to inspect the raw HTTP exchange, run Fiddler or use the
-Verboseswitch.
The basic GET request
The simplest possible call. You hand it a URI and it gives you back an object you can pipe into anything else.
$baseUri = 'https://jsonplaceholder.typicode.com'
# Pull all posts
Invoke-RestMethod -Method Get -Uri "$baseUri/posts"
# Pull a specific post
Invoke-RestMethod -Method Get -Uri "$baseUri/posts/42"
# Pull nested resources
Invoke-RestMethod -Method Get -Uri "$baseUri/posts/42/comments"
What I like about this cmdlet over Invoke-WebRequest + ConvertFrom-Json is that the parsing is done for you. The result is a regular PowerShell object — pipe it to Get-Member and you’ll see real properties, not a string blob.
Query parameters
You can append query strings directly to the URL or build them as a hashtable. For one or two parameters, inline is fine. For more than that, build a hashtable and let PowerShell encode it.
Invoke-RestMethod -Method Get -Uri "$baseUri/posts?userId=3"
POSTing JSON data
Sending data is where the cmdlet really earns its keep. Build a hashtable, convert it to JSON, send it as the body. Don’t forget the -ContentType — without it the API will usually 415 you.
$newUser = @{
name = 'John Doe'
email = '[email protected]'
} | ConvertTo-Json
$response = Invoke-RestMethod -Method Post `
-Uri "$baseUri/users" `
-Body $newUser `
-ContentType 'application/json'
$response
One gotcha: ConvertTo-Json defaults to a depth of 2. If your object has nested arrays or sub-objects more than two levels deep, pass -Depth 10 or higher. We had a client’s webhook silently dropping fields for a week before I caught this on a deeply nested order payload.
PUT, PATCH, and DELETE
Same pattern, different verb. DELETE usually has no body at all.
# Update
Invoke-RestMethod -Method Put -Uri "$baseUri/posts/42" -Body $jsonBody -ContentType 'application/json'
# Delete (no body needed)
Invoke-RestMethod -Method Delete -Uri "$baseUri/posts/42"
Headers and authentication
Most production APIs need a bearer token or API key. Headers go in as a hashtable. If you’re calling something like Azure Resource Manager, the URL gets a subscription ID and an api-version query parameter, and the auth token rides in the header.
$headers = @{
'Authorization' = "Bearer $token"
'Accept' = 'application/json'
}
Invoke-RestMethod -Method Get `
-Uri 'https://management.azure.com/subscriptions/xxxx/resourcegroups?api-version=2021-04-01' `
-Headers $headers
For long-running scripts I always pull the token from a secrets vault rather than embedding it. When we standardized this for a client’s tenant in 2024, we moved every script to pull from Azure Key Vault on startup — one change point if a key rotates, no scripts to chase down.
Error handling that actually helps
The default behavior on a non-2xx response is to throw a terminating error, but only if you set -ErrorAction Stop. Wrap calls in try/catch and inspect the exception — the response body usually tells you exactly what’s wrong.
try {
$data = Invoke-RestMethod -Uri "$baseUri/posts/99999" `
-Method Get -ErrorAction Stop
}
catch {
Write-Warning "API call failed: $($_.Exception.Message)"
Write-Warning "Status: $($_.Exception.Response.StatusCode.value__)"
}
For anything customer-facing, I always log the status code and the first 500 characters of the response body. Generic “the API failed” log entries are useless at 2am when you’re trying to figure out whether the vendor is down or your token expired.
Pagination and rate limiting
This is the part most tutorials skip. Real APIs paginate and they rate-limit. Your script needs to handle both or it will fall over on day three.
$page = 1
$allResults = @()
do {
$resp = Invoke-RestMethod -Uri "$baseUri/posts?_page=$page&_limit=20" -Method Get
$allResults += $resp
$page++
Start-Sleep -Milliseconds 250 # be polite
} while ($resp.Count -eq 20)
The Start-Sleep is not optional. We had a client’s script get their service account temporarily banned from a SaaS API because it hammered the endpoint with 4,000 requests in under a minute. The vendor’s docs said “reasonable rate” — turns out that meant about 60 per minute. Read the rate limit docs first.
A real workflow example
Here’s the pattern I keep coming back to: query an API, compare the result to local state, take action. This is the core of half the automation we run for managed clients.
$apiUrl = 'https://api.example.com/software/latest'
$latest = Invoke-RestMethod -Uri $apiUrl -Method Get
if ($latest.Version -ne $currentVersion) {
Write-Host "New version $($latest.Version) available — triggering update"
Start-Job -ScriptBlock { & C:\Scripts\Update-App.ps1 -Version $using:latest.Version }
}
If you’re running this kind of polling logic across a whole fleet, pair it with Invoke-Command at scale so the same check runs against 200 endpoints in parallel.
When NOT to use Invoke-RestMethod
If you need to download a binary file, capture response headers, follow redirects manually, or upload multipart form data, reach for Invoke-WebRequest instead. It gives you the full response object — headers, status codes, raw content — at the cost of doing your own JSON parsing. I also use it any time I’m debugging a flaky API because the raw response tells you things Invoke-RestMethod hides.
And if you’re integrating against something that produces or consumes large structured datasets — log archives, email metadata, audit feeds — consider whether you actually need to roll your own. For things like email archiving ingestion, vendor connectors usually beat a homegrown PowerShell script for reliability and supportability.
Takeaway
Treat Invoke-RestMethod as your default for anything JSON-based. Build a small helper function that wraps it with your standard headers, retry logic, and logging — you’ll use it everywhere. Add -ErrorAction Stop, set a sane -Depth on ConvertTo-Json, respect rate limits, and don’t hardcode tokens. That covers about 90% of the production issues I’ve seen with PowerShell API scripts. If you need help wiring this kind of automation into a managed environment, get in touch and we can walk through your setup.


