Integrating With REST APIs From PowerShell — Invoke-RestMethod in Practice
· Go Komura · PowerShell, REST API, Windows, Automation, Business Systems, Integration, JSON, Operational Improvement
“Fetch order data from the core system’s Web API and drop it into an in-house Excel report.” “Hit a SaaS attendance API every morning and output the day’s shift plan.” Among the situations where PowerShell gets used in business, REST API integration is the one that has clearly grown over the past few years. Not worth buying a dedicated tool for, but impossible to keep doing by hand. As the tool that fills that gap, Invoke-RestMethod is extremely powerful.
At the same time, it is characteristic of API integration that getting it working is easy but putting it into production suddenly gets hard. Japanese text turns into mojibake, the contents of error responses can’t be read, it occasionally fails with a 429, it won’t get through in a proxy environment, and it gets rejected at the TLS layer on Windows PowerShell 5.1 only. Every one of these is a problem peculiar to “a system with another party on the other end”.
This article is aimed at IT staff and developers calling APIs from PowerShell in-house, and organises authentication, sending and receiving JSON, error handling, retries, paging, and the 5.1-specific pitfalls, in the order you need them in practice.
1. The Bottom Line First
- For JSON/XML APIs, use
Invoke-RestMethod. It turns the response into objects automatically. If you need the status code or headers, useInvoke-WebRequest, or-StatusCodeVariable/-ResponseHeadersVariable.12 - Passing authentication in a header is the most universally applicable approach. From PowerShell 6 onwards you can also use
-Authentication Bearer -Token(a SecureString).1 - Sending non-ASCII JSON as a UTF-8 byte array is the reliable option. State
charset=utf-8explicitly in-ContentType. ConvertTo-Jsondefaults to a depth of 2. Deeply nested objects get truncated unless you specify-Depth.3- 4xx/5xx become terminating errors. From PowerShell 7 onwards you can read the body with
$_.ErrorDetails.Message.-SkipHttpErrorCheckis an option for stopping them being raised as exceptions.1 - For 429, wait as instructed by
Retry-After. Specify-MaximumRetryCountand the built-in behaviour follows it automatically. Note, however, that the built-in retry covers everything in the 400–599 range (plus 304), so 401 and 404 get resent too. Implement your own only when you need per-code handling or exponential backoff.1 - Do not automatically retry non-idempotent requests such as
POST. Even on a communication error or a 5xx, the server may already have processed it, and resending creates a duplicate record. Link-header paging can be automated with-FollowRelLink. Cursor-based paging needs your own loop.1- Windows PowerShell 5.1 has its own pitfalls. Three of them: whether
-UseBasicParsingis needed, explicitly enabling TLS 1.2, and how encoding is handled.4 - Don’t use
-SkipCertificateCheckin permanent operation. Making your internal CA trusted is the correct fix.
2. Invoke-RestMethod and Invoke-WebRequest
Invoke-RestMethod |
Invoke-WebRequest |
|
|---|---|---|
| Handling of the response | JSON/XML is converted to objects automatically | WebResponseObject (raw body, headers, code) |
| Main use | REST APIs | Fetching HTML, inspecting status and headers |
| Status code | Obtained via -StatusCodeVariable (PS7+) |
.StatusCode |
| Headers | Obtained via -ResponseHeadersVariable (PS6+) |
.Headers |
For calling APIs, Invoke-RestMethod is the default choice. Even when you need headers or codes, you can get them via the dedicated variable parameters.
$data = Invoke-RestMethod -Uri 'https://api.example.co.jp/v1/orders' `
-Headers @{ Authorization = "Bearer $token" } `
-StatusCodeVariable status -ResponseHeadersVariable headers -TimeoutSec 30
"HTTP $status / requests remaining: $($headers['X-RateLimit-Remaining'])"
$data.items | Select-Object orderId, customerName, amount
3. Passing Authentication
The most universally applicable method is writing it directly into the header, which works the same way on both 5.1 and 7.
# (1) Bearer token (the most common)
$headers = @{ Authorization = "Bearer $accessToken"; Accept = 'application/json' }
Invoke-RestMethod -Uri $uri -Headers $headers
# (2) API key (the header name follows the provider's specification)
$headers = @{ 'X-Api-Key' = $apiKey }
# (3) Basic authentication (-Authentication is available from PowerShell 6 onwards)
Invoke-RestMethod -Uri $uri -Authentication Basic -Credential $cred
# (4) Passing a Bearer token via -Token (PowerShell 6 onwards; can be a SecureString)
Invoke-RestMethod -Uri $uri -Authentication Bearer -Token $secureToken
# (5) Client certificate
Invoke-RestMethod -Uri $uri -Certificate $cert
When you use -Authentication, PowerShell refuses by default to use it over anything other than HTTPS (you can get around this with -AllowUnencryptedAuthentication, but you shouldn’t, since credentials would travel in the clear).1
Never writing tokens or API keys directly into a script goes without saying. Storage using SecretManagement is covered in “Handling Credentials Safely in PowerShell”.
4. Sending JSON — The Mojibake and -Depth Traps
The two things you will definitely hit when sending are mojibake and truncated nesting.
ConvertTo-Json’s -Depth defaults to 2, and levels deeper than that are not expanded — they get replaced with things like the type name string.3 Always specify it for request bodies with nesting.
$body = @{
order = @{
customer = @{ code = 'C001'; name = '株式会社サンプル' } # third level
lines = @( @{ item = 'A-100'; qty = 3 } )
}
}
# [BAD] with the default -Depth 2, the contents of customer and lines are lost
$json = $body | ConvertTo-Json
# [GOOD] specify a sufficient depth
$json = $body | ConvertTo-Json -Depth 10
For mojibake, converting to a UTF-8 byte array before sending is the most reliable approach.
$json = $body | ConvertTo-Json -Depth 10
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
$res = Invoke-RestMethod -Uri $uri -Method Post `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType 'application/json; charset=utf-8' `
-Body $bytes -TimeoutSec 60
On PowerShell 7 a string passed as-is is sent as UTF-8, but in scripts shared with 5.1, converting to a byte array absorbs the environmental difference. For the broader picture of character encodings on Windows, see “Windows Text Encodings and Line Endings”.
5. Error Handling — You Can’t Investigate What You Can’t Read
Invoke-RestMethod treats 4xx/5xx responses as terminating errors. That means you can catch them with try/catch, but the question becomes how to read the error message body the API returned.
try {
$res = Invoke-RestMethod -Uri $uri -Method Post -Body $bytes `
-ContentType 'application/json; charset=utf-8' -TimeoutSec 30
}
catch {
$status = $_.Exception.Response.StatusCode # e.g. BadRequest / 400
# From PowerShell 7 onwards the response body lands here (the API's error message)
$detail = $_.ErrorDetails.Message
Write-Warning "API call failed ($status): $detail"
throw
}
So that you don’t burn time on investigations of the form “we got an HTTP 400 back, but we have no idea why it was rejected”, design your scripts to always log the error body.
When you want to branch on the status code, using -SkipHttpErrorCheck to stop exceptions being raised makes for more straightforward code (PowerShell 7 onwards).1
$res = Invoke-RestMethod -Uri $uri -SkipHttpErrorCheck -StatusCodeVariable code -TimeoutSec 30
switch ($code) {
200 { $res.items }
404 { Write-Warning 'The target does not exist'; @() }
{ $_ -ge 500 } { throw "Server-side error: $code" }
default { throw "Unexpected response: $code" }
}
6. Retries — 429 and Transient Failures
From PowerShell 6 onwards, Invoke-RestMethod has -MaximumRetryCount and -RetryIntervalSec, which retry on failure. What’s more, when a 429 response includes Retry-After, that header’s value is used instead of the interval you specified.1 In other words, if all you want is “when rate limited, wait as instructed and retry”, the built-in feature is enough.
# For rate limiting alone, this is often all you need
Invoke-RestMethod -Uri $uri -Headers $headers -MaximumRetryCount 4 -RetryIntervalSec 5
However, 429 is not the only thing that gets retried. The documentation states that it retries “when it receives a failure code between 400 and 599 (and 304)”.1 That means requests that will never succeed no matter how many times you send them — 401 (authentication error), 403 (insufficient permissions), 404 (wrong URL) — get retried too. Run unattended with a misconfigured token and you waste -MaximumRetryCount × -RetryIntervalSec seconds before you even learn it failed, while the API receives the same invalid request over and over. If you want to stop immediately on a permanent error, you need the custom implementation that follows.
You need your own retry function when you have requirements like these.
- Treating status codes differently (fail immediately on 4xx, retry only 5xx, and so on)
- Using exponential backoff (the built-in retries at the interval you specify)
- Logging the API’s error body on failure
Here is what that looks like.
function Invoke-KsApi {
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $Uri,
[string] $Method = 'Get',
[object] $Body,
[hashtable] $Headers = @{},
[ValidateRange(1, 10)] [int] $MaxAttempts = 4,
# If Retry-After instructs a longer wait than this, abort instead of waiting
[ValidateRange(1, 86400)] [int] $MaxWaitSeconds = 300,
# Allow retrying POST and similar only when the API supports idempotency keys
[string] $IdempotencyKey
)
# Retrying is only acceptable when receiving the same request twice doesn't change
# the outcome. POST/PATCH can end up in the "the server succeeded but the response
# never arrived" case, where a naive resend creates a duplicate record
$idempotentMethods = 'Get', 'Head', 'Options', 'Put', 'Delete'
$canRetry = ($Method -in $idempotentMethods) -or $IdempotencyKey
if ($IdempotencyKey) { $Headers = $Headers + @{ 'Idempotency-Key' = $IdempotencyKey } }
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
$params = @{
Uri = $Uri
Method = $Method
Headers = $Headers
TimeoutSec = 60
SkipHttpErrorCheck = $true # we want to branch on the code, so no exception
StatusCodeVariable = 'code'
ResponseHeadersVariable = 'resHeaders'
}
# So that a $Body value of $false, 0 or an empty string can still be sent as a body,
# test whether the argument was passed rather than testing for truthiness
if ($PSBoundParameters.ContainsKey('Body')) {
# Passing it through the pipe makes an empty array @() count as "zero input"
# and return $null, so convert it directly with -InputObject (@() becomes [])
$json = ConvertTo-Json -InputObject $Body -Depth 10
$params.Body = [System.Text.Encoding]::UTF8.GetBytes($json)
$params.ContentType = 'application/json; charset=utf-8'
}
# -SkipHttpErrorCheck only suppresses HTTP error responses. Communication errors
# where no response comes back at all - timeouts, name resolution failures,
# connection resets, TLS errors - are still thrown, so catch them here and retry
try {
$code = $null
$res = Invoke-RestMethod @params
}
catch {
# For a non-idempotent request, the server may have succeeded and only the
# response failed to arrive. Don't resend automatically; leave the decision to the caller
if (-not $canRetry) {
throw "Communication error ($Method is not retried; please check whether it was processed): $($_.Exception.Message)"
}
if ($attempt -eq $MaxAttempts) { throw }
$wait = [math]::Min([math]::Pow(2, $attempt), 60)
Write-Warning "Communication error: $($_.Exception.Message) - retrying in $wait seconds ($attempt/$MaxAttempts)"
Start-Sleep -Seconds $wait
continue
}
if ($code -lt 400) { return $res } # success
# Only transient errors are worth retrying. Make it explicit with an allow list
# (retrying permanent errors such as 405 or 415 wastes time and leaves you with
# nothing but an irrelevant "retry limit" message at the end)
$retryable = @(408, 429, 500, 502, 503, 504)
if ($code -notin $retryable) {
throw "API error ($code): $($res | ConvertTo-Json -Compress -Depth 3)"
}
# A 5xx may mean "the server processed it and then failed", so don't resend
# non-idempotent requests. The same applies to 429 (no guarantee it was rejected before processing)
if (-not $canRetry) {
throw "API error ($code). $Method is not retried automatically: $($res | ConvertTo-Json -Compress -Depth 3)"
}
if ($attempt -eq $MaxAttempts) {
throw "Retry limit reached ($code): $($res | ConvertTo-Json -Compress -Depth 3)"
}
# Retry-After can come back not only as a number of seconds but in HTTP-date form.
# Casting that straight to [int] throws, taking the whole retry down with it
$wait = $null
$retryAfter = if ($resHeaders) { $resHeaders['Retry-After'] | Select-Object -First 1 }
if ($retryAfter) {
$seconds = 0
$date = [datetime]::MinValue
if ([int]::TryParse($retryAfter, [ref] $seconds)) {
$wait = $seconds
}
elseif ([datetime]::TryParse($retryAfter,
[cultureinfo]::InvariantCulture,
[System.Globalization.DateTimeStyles]::AdjustToUniversal, [ref] $date)) {
$wait = [math]::Max(0, [int]($date - [datetime]::UtcNow).TotalSeconds)
}
}
if ($null -eq $wait) {
$wait = [math]::Min([math]::Pow(2, $attempt), 60) # exponential backoff (capped at 60s)
}
elseif ($wait -gt $MaxWaitSeconds) {
# Truncating the server's instruction and resending early just repeats the 429
# until you hit the limit. If the wait is too long, return it to the caller with the wait time
throw "Rate limited. The server's instructed wait of $wait seconds exceeds the limit of $MaxWaitSeconds seconds, so the call was aborted. Please try again later ($code)"
}
Write-Warning "HTTP $code - retrying in $wait seconds ($attempt/$MaxAttempts)"
Start-Sleep -Seconds $wait
}
}
Handling idempotency is the second key point. GET and PUT give the same result when the same request is received twice, but POST does not. In particular, in the case where “the server registered the record successfully, but the connection dropped before the response came back”, a naive resend produces a duplicate record. The implementation above allows automatic retries only for idempotent methods, or when the API supports an idempotency key (Idempotency-Key), and otherwise stops with an explicit “please check whether it was processed”.
Not truncating Retry-After is another important point. If the server is telling you to come back in 30 minutes and you resend after 5 minutes because of your own limit, all you get back is the same 429. You burn your attempts on pointless requests and are left, at the end, with nothing but a “retry limit” message. In the implementation above, when the instructed wait exceeds -MaxWaitSeconds, it fails immediately with the wait time attached, rather than waiting for a shorter period and trying again. In batch processing, the caller can catch this exception and choose either to carry the work over to the next run or to wait exactly as instructed.
Another key point is that the retryable set is spelled out as an allow list. Write it as “enumerate the permanent errors and exclude them” and any code you forget to list (405 Method Not Allowed, 415 Unsupported Media Type and so on) gets treated as transient, so you repeat a request that was never going to succeed and end up with nothing but an unexplained “retry limit” message. Retry only the codes you know to be transient, and fail immediately on everything else, error body and all. For retry design in general, see “PowerShell Error Handling and Retry Design”.
7. Paging
An API will almost never return everything at once. There are two main schemes.
(1) The Link header scheme (used by GitHub among others) lets you follow the next page automatically with -FollowRelLink.1
# Follow the next page automatically (you can also cap the number of pages fetched)
$all = Invoke-RestMethod -Uri $uri -Headers $headers -FollowRelLink -MaximumFollowRelLink 20
(2) The cursor/offset scheme requires your own loop.
$items = [System.Collections.Generic.List[object]]::new()
$cursor = $null
$page = 0
$maxPages = 100
do {
$page++
# The separator depends on whether the original URI already has a query string.
# Always appending ? gives .../items?status=active?cursor=..., and to the server
# the cursor looks like part of the value of status.
# The cursor itself is an opaque string (it may contain + & = # etc.), so always encode it
$u = if ($cursor) {
$sep = if ($uri.Contains('?')) { '&' } else { '?' }
"$uri$sep" + "cursor=$([uri]::EscapeDataString($cursor))"
}
else { $uri }
$res = Invoke-KsApi -Uri $u -Headers $headers
$items.AddRange([object[]]$res.items)
$cursor = $res.nextCursor
# Always announce a truncation. Bailing out silently makes people think they got everything
if ($page -ge $maxPages -and $cursor) {
Write-Warning "Reached the page limit ($maxPages) and stopped. Some records may be missing"
break
}
} while ($cursor)
"Records fetched: $($items.Count)"
List[T] is used for $items to avoid the array being rebuilt by += (see “Where to Look When a PowerShell Script Is Slow”).
Don’t forget to always set a page limit either. Bugs where the server keeps returning the same cursor, or forgets to blank out nextCursor, really do happen. Without a limit, the script never stops and just keeps firing requests. And the fact that it was truncated needs to be surfaced as a warning. Break out silently and the caller will process an incomplete result believing it to be the full set.
8. Pitfalls Specific to Windows PowerShell 5.1
In environments where 5.1 remains, these are the first three things to suspect.4
(1) TLS 1.2 is not enabled. Left on the old default settings, you cannot connect to APIs that accept only TLS 1.2 or higher, and you get “The underlying connection was closed”.
# The incantation for Windows PowerShell 5.1 (at the top of the script)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
(2) -UseBasicParsing may be required. 5.1’s Invoke-WebRequest parses HTML using the Internet Explorer engine by default, so it fails under accounts where IE has never been initialised (service accounts, for example). From PowerShell 6 onwards this dependency is gone and -UseBasicParsing is ignored even if specified.4
(3) -SkipHttpErrorCheck and -Authentication don’t exist. On 5.1 you have to read the response stream yourself in order to read an error body. If you are going to do API integration seriously, introducing PowerShell 7 is the cheapest fix (see “Differences Between Windows PowerShell 5.1 and PowerShell 7”).
9. Proxies and Certificates
When calling an API on the internet from inside a corporate network, getting through the proxy is the first hurdle.
# Specify the proxy explicitly and authenticate with the logged-on user's credentials
Invoke-RestMethod -Uri $uri -Proxy 'http://proxy.example.co.jp:8080' -ProxyUseDefaultCredentials
When you run under a Task Scheduler service account, the proxy settings can differ from those during an interactive logon. This is the classic “it works on my machine but only the overnight batch fails” pattern. Match the execution account when you test (see “When Task Scheduler Tasks Don’t Run”).
Using -SkipCertificateCheck for certificate errors should be limited to a temporary workaround in a test environment. The permanent fix is to place your internal CA’s certificate in the Trusted Root Certification Authorities store and issue server certificates properly.
10. Standard Practice in the Field (Decision Table)
| Issue | Options | Rule of thumb |
|---|---|---|
| Cmdlet | Invoke-RestMethod / Invoke-WebRequest |
The former for JSON APIs. Get headers and codes via the dedicated variables12 |
| Authentication | Header written directly / -Authentication |
Use the header approach if sharing with 5.1. Store the values in SecretManagement |
| Sending JSON | String / UTF-8 byte array + explicit charset | Avoids mojibake caused by environmental differences |
ConvertTo-Json |
Default / specify -Depth |
The default is 2. Always specify it for nesting3 |
| Errors | try/catch only / log the body as well | $_.ErrorDetails.Message (PS7). It decides whether you can find the cause1 |
| Lots of branching | catch / -SkipHttpErrorCheck + code branching |
The latter is more natural when the status decides the processing1 |
| Retries | -MaximumRetryCount / your own |
The built-in follows 429’s Retry-After automatically. But it retries everything from 400 to 599, so roll your own when you want permanent errors to fail immediately1 |
| Retrying POST | Only when there is an idempotency key | Registration may have succeeded with only the response lost, so a naive resend creates a duplicate |
| Paging | -FollowRelLink / your own loop |
The former for the Link header scheme1 |
| 5.1 environments | As-is / explicit TLS 1.2 + consider PS7 | Most connection errors come down to TLS settings4 |
| Certificate errors | -SkipCertificateCheck / trust the internal CA |
Never disable validation in permanent operation |
11. Summary
Invoke-RestMethodis the default for JSON APIs. Headers and status codes are available via-ResponseHeadersVariable/-StatusCodeVariable.- Send non-ASCII JSON as a UTF-8 byte array and state
charset=utf-8explicitly. ForgettingConvertTo-Json -Depthleads to missing nested content. - 4xx/5xx are terminating errors. Read the body with
$_.ErrorDetails.Messageand log it. If you have a lot of branching,-SkipHttpErrorCheckis easier to work with. - Do not automatically retry non-idempotent requests such as
POST. A communication error or a 5xx may mean “the server already processed it”, and resending creates a duplicate record. Retrying requires an idempotency key mechanism. - For 429, the basic approach is to wait as instructed by
Retry-After. Use-MaximumRetryCountand this is handled by the built-in behaviour. But since the built-in retries everything from 400 to 599, you need your own retry logic if you want 401 or 404 to fail immediately. The same goes for per-code handling and exponential backoff — permanent errors should fail immediately rather than being retried. - For paging, use
-FollowRelLinkfor theLinkheader scheme and your own loop for the cursor scheme. Don’t use+=to accumulate the results. - In 5.1 environments the three obstacles are explicit TLS 1.2, the IE engine dependency, and missing features. If you’re going to keep doing API integration, introducing PowerShell 7 is the quickest resolution.
Download the Sample Code
The code covered in this article is packaged in ready-to-run form. It includes an API caller implementing retries, idempotency and paging, plus an HTTP server for testing.
Download the sample code (zip)
The samples in this article have been actually executed and verified on PowerShell 7.6 (21 Pester tests). Run the Invoke-SampleTests.ps1 included in the zip and you can reproduce the same verification on your own machine.
# Syntax parsing + static analysis + Pester tests
./Invoke-SampleTests.ps1
The configuration values (paths, server names, tenant IDs and so on) are examples. Do not run them against a production environment as they are — adapt them to your own environment.
Related Articles
- PowerShell Error Handling and Retry Design — From the try/catch Trap to Exit Codes and Retry Best Practices
- Handling Credentials Safely in PowerShell — Getting Plaintext Passwords Out of Your Scripts
- Differences Between Windows PowerShell 5.1 and PowerShell 7 — A Practical Guide to Migrating In-House Scripts
- Where to Look When a PowerShell Script Is Slow — Arrays, Pipelines and Matching
- Getting Started With Microsoft Graph PowerShell — Managing Microsoft 365 After the Retirement of AzureAD and MSOnline
- Don’t Wrap HttpClient in a using Block — Practical HTTP Communication in C# Business Apps
Related Consulting Areas
KomuraSoft LLC handles the design and implementation of in-house integrations using core-system and SaaS APIs, automation that replaces existing manual work with API integration, and investigation of communication-related defects.
- Business Application Development
- Technical Consulting & Design Review
- Bug Investigation & Root-Cause Analysis
- Contact Us
References
-
Microsoft Learn, Invoke-RestMethod. On sending requests to REST endpoints and converting the response’s JSON/XML into PowerShell objects; on specifying -Headers / -Body / -ContentType / -Method; on -Authentication (Basic / Bearer / OAuth) and -Token, and authentication over anything but HTTPS being refused by default; on -SkipHttpErrorCheck making 4xx/5xx non-exceptional; on retrieval via -StatusCodeVariable and -ResponseHeadersVariable; on retries via -MaximumRetryCount / -RetryIntervalSec; on Link-header paging via -FollowRelLink / -MaximumFollowRelLink; and on -Proxy / -ProxyUseDefaultCredentials, -SkipCertificateCheck and -TimeoutSec. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16
-
Microsoft Learn, Invoke-WebRequest. On returning the response as a WebResponseObject giving access to StatusCode, Headers and Content; on Windows PowerShell 5.1 parsing HTML with the Internet Explorer engine by default and -UseBasicParsing being the way around it; and on PowerShell 6 onwards dropping the IE dependency so that -UseBasicParsing is ignored. ↩ ↩2 ↩3
-
Microsoft Learn, ConvertTo-Json. On -Depth defaulting to 2 and levels deeper than that not being converted, and on -Compress removing whitespace. ↩ ↩2 ↩3
-
Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. On behavioural differences in the web-related cmdlets, constraints specific to Windows PowerShell 5.1, and features added in PowerShell 7. Also the ServicePointManager.SecurityProtocol property on specifying the TLS version. ↩ ↩2 ↩3 ↩4
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Automating PC Provisioning With winget + PowerShell — Making the Runbook Executable
How to make new-hire PC setup reproducible. Covers installing applications with winget and export/import, declarative configuration with ...
Where to Look When a PowerShell Script Is Slow — Arrays, Pipelines and Matching
The classic causes of slow PowerShell scripts, laid out. Why += on an array is O(n^2), the difference between the pipeline and foreach, t...
Stop Using Write-Host — PowerShell Output Streams and Log Design
How to choose between PowerShell's six output streams, the problems with Write-Host and where it genuinely belongs, why function return v...
Parallel Processing in PowerShell — Choosing Between ForEach-Object -Parallel and Jobs
A practical rundown of the differences between ForEach-Object -Parallel, Start-ThreadJob and Start-Job and when to use each, $using: and ...
Calling External EXEs Correctly from PowerShell — The Pitfalls of Argument Quoting, Exit Codes, and Mojibake
Call robocopy or an in-house EXE from PowerShell and the arguments break, the exit code is unavailable, and the output turns into mojibak...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Frequently Asked Questions
Common questions about the topic of this article.
- How do I choose between Invoke-RestMethod and Invoke-WebRequest?
- For handling an API's JSON or XML, use Invoke-RestMethod. It parses the response body automatically and converts it into PowerShell objects, so you don't have to call ConvertFrom-Json yourself. Invoke-WebRequest returns the response as an HtmlWebResponseObject, giving you access to the status code, headers and raw body. Choose Invoke-WebRequest when you want to inspect the status code or headers, or when you want to work with HTML directly. Note that from PowerShell 6 onwards Invoke-RestMethod has -ResponseHeadersVariable and -StatusCodeVariable, so if all you need are headers or the code, you can stay with Invoke-RestMethod.
- When I send JSON containing Japanese, it arrives as mojibake at the other end.
- This happens because the byte sequence of the body and the declaration in Content-Type disagree. The reliable approach is to convert the string produced by ConvertTo-Json into a UTF-8 byte array, pass that to -Body, and state charset=utf-8 explicitly in -ContentType. On Windows PowerShell 5.1 a string passed as-is can be sent using the default encoding, so this fix is particularly effective there. Mojibake on the receiving side has the same root cause — check whether the response's charset is declared correctly.
- When the API returns 404 or 500, I want to read the error message in the response body.
- On PowerShell 7 and later, look at $_.ErrorDetails.Message in the catch block — the response body is in there. The status code is available from $_.Exception.Response.StatusCode. In addition, adding -SkipHttpErrorCheck means 4xx/5xx responses are received as normal responses rather than raised as exceptions, which makes life easier when you want to branch on the status code. On Windows PowerShell 5.1 you have to read the response stream yourself, which is another reason we recommend using 7.
- The API is returning 429 (rate limited). How should I handle it?
- The basic approach is to wait for the number of seconds indicated by the response's Retry-After header and then retry. If the header is absent, widen the interval with exponential backoff (2 seconds, 4 seconds, 8 seconds and so on). From PowerShell 6 onwards there are -MaximumRetryCount and -RetryIntervalSec, and when a 429 response includes Retry-After the header's value is used instead of the interval you specified — so for rate limiting alone, the built-in behaviour is enough. Use your own retry function only when you need to treat different status codes differently or need exponential backoff. Also, retrying a non-idempotent request such as a POST risks creating duplicate records, so avoid it unless there is an idempotency key mechanism. Fundamentally, reducing the number of calls in the first place (fetching only the fields you need, using bulk-retrieval APIs) is the surer fix.
- I get an error with an internal API's self-signed certificate. Is it OK to use -SkipCertificateCheck?
- Avoid it in permanent operation. Turning off certificate validation means you are not confirming that the party you are talking to is genuine, which leaves room for a man-in-the-middle attack even on an internal network. The correct fix is to place your internal CA's certificate in the execution environment's Trusted Root Certification Authorities store and issue server certificates properly. Even when using it temporarily in a test environment, make it an explicit switch via a configuration file or a parameter so that it never leaks into a production script.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links