Web APIs, JSON and XML
Talk to web services with Invoke-RestMethod, handle paging and errors, and read, query and edit XML.
- Call REST APIs with Invoke-RestMethod - GET and POST, headers, query strings and JSON bodies
- Handle paging and HTTP errors, and know when to reach for Invoke-WebRequest
- Load XML with [xml], query it with dot notation and Select-Xml, edit it, and compare lists with Compare-Object
The Emberfall weather station, the vet’s booking system and the dragon registry all speak HTTP APIs - they answer web requests with JSON instead of web pages. Invoke-RestMethod (alias irm) sends the request and converts the JSON answer into objects, so a web service feels like any other cmdlet.
Most APIs want a few extras: an API key or token in a header, parameters in the query string (?species=wisp&page=2), and for POST/PUT requests a JSON body. Splatting keeps all of that readable.
1$headers = @{ Authorization = "Bearer $env:EMBERFALL_TOKEN" }
2
3# GET: read data
4$dragons = Invoke-RestMethod -Uri 'https://api.emberfall.example/dragons?species=wisp' -Headers $headers
5$dragons | Select-Object name, wingspan
6
7# POST: send data as a JSON body
8$request = @{
9 Uri = 'https://api.emberfall.example/dragons'
10 Method = 'Post'
11 Headers = $headers
12 ContentType = 'application/json'
13 Body = @{ name = 'Pebble'; species = 'Wyrm'; wingspan = 3 } | ConvertTo-Json
14}
15$created = Invoke-RestMethod @request
16"registered with id $($created.id)"You can practice all of the parsing without a network: an API response is just JSON text. Build query strings with [uri]::EscapeDataString() so spaces and & don’t break the URL, and use an [ordered] hashtable when the order of fields in a body matters to you.
1$response = @'
2{ "page": 1, "dragons": [
3 { "name": "Skyla", "wingspan": 15, "tags": ["storm", "fast"] },
4 { "name": "Glim", "wingspan": 1, "tags": [] }
5] }
6'@ | ConvertFrom-Json
7$response.dragons.Count
8$response.dragons[0].tags -join ', '
9$response.dragons | Sort-Object wingspan | ForEach-Object name
10$search = 'fire & ice'
11"https://api.emberfall.example/search?q=$([uri]::EscapeDataString($search))"
12[ordered]@{ name = 'Pebble'; wingspan = 3 } | ConvertTo-Json -Compress2
storm, fast
Glim
Skyla
https://api.emberfall.example/search?q=fire%20%26%20ice
{"name":"Pebble","wingspan":3}Paging and errors
APIs rarely return everything at once. They send a page and tell you where the next one is - a next link, or a page number to increase. Loop until there’s no next page.
When the server answers with an error status (404 Not Found, 401 Unauthorized, 500 Server Error), Invoke-RestMethod throws a terminating error - so try/catch works, and $_.Exception.Response.StatusCode tells you which. Follow a request and its errors step by step:
Try it
Follow the requests
Step through a script that fetches every page of dragons, then looks up one that doesn’t exist. Predict before each reveal.
1$all = [System.Collections.Generic.List[object]]::new()
2$uri = 'https://api.emberfall.example/dragons?page=1'
3while ($uri) {
4 $page = Invoke-RestMethod -Uri $uri -Headers $headers
5 $all.AddRange(@($page.dragons))
6 $uri = if ($page.next) { "https://api.emberfall.example$($page.next)" }
7}
8"fetched $($all.Count) dragons"
9
10try {
11 Invoke-RestMethod -Uri 'https://api.emberfall.example/dragons/smaug' -Headers $headers
12} catch {
13 $status = [int]$_.Exception.Response.StatusCode
14 if ($status -eq 404) { 'No such dragon - carry on' } else { throw }
15}XML
Older systems, config files (web.config, .csproj), RSS feeds and many government datasets speak XML. Cast text with [xml] and PowerShell gives you an XmlDocument you can walk with dots: elements and attributes both become properties.
For searching, Select-Xml takes an XPath query: //dragon means “every dragon element anywhere”, [@species="Wisp"] filters by attribute. Edit with CreateElement(), SetAttribute() and AppendChild(), then .Save($path) (use a full path).
1[xml]$census = @'
2<census date="2026-03-14">
3 <dragon name="Ember" species="Firedrake" wingspan="12" />
4 <dragon name="Glim" species="Wisp" wingspan="1" />
5 <dragon name="Skyla" species="Stormwing" wingspan="15" />
6</census>
7'@
8$census.census.date
9$census.census.dragon.Count
10$census.census.dragon | Where-Object { [int]$_.wingspan -gt 10 } | ForEach-Object name
11(Select-Xml -Xml $census -XPath '//dragon[@species="Wisp"]').Node.name
12($census.census.dragon | Measure-Object -Property wingspan -Sum).Sum
13$pebble = $census.CreateElement('dragon')
14$pebble.SetAttribute('name', 'Pebble')
15[void]$census.census.AppendChild($pebble)
16$census.census.dragon.Count2026-03-14 3 Ember Skyla Glim 28 4
Compare-Object answers “what changed between these two lists?” - perfect for comparing yesterday’s census with today’s. <= means only in the first (reference) list, => only in the second (difference) list, and -IncludeEqual adds == for items in both.
1$monday = 'Ember', 'Glim', 'Skyla', 'Pebble'
2$tuesday = 'Ember', 'Skyla', 'Pebble', 'Nimbus'
3Compare-Object -ReferenceObject $monday -DifferenceObject $tuesday |
4 ForEach-Object { "$($_.InputObject) $($_.SideIndicator)" }Nimbus => Glim <=
Key takeaways
Invoke-RestMethodsends HTTP requests and turns JSON replies into objects; splat Uri, Method, Headers, ContentType and Body.Escape query values with
[uri]::EscapeDataString(); loop for pages; error statuses throw, so use try/catch.Keep tokens in environment variables or a secret vault, never in the script.
[xml]plus dot notation reads XML,Select-Xmlqueries it with XPath, andCompare-Objectshows what changed between two lists.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: write PowerShell scripts
Write a script in the editor and run it for real against sample input, which is piped into your script as $input. Scripts run on PowerShell 6.2 through Try It Online (tio.run), a free public service, so these exercises avoid PowerShell 7-only syntax; your script and test input are sent there.
Read an API page
The input is one page of JSON from the dragon registry, with page, totalPages, next (a URL or null) and a dragons array of objects with name, wingspan and status.
Print page 1 of 2, then each dragon as Skyla (15 m) - flying, widest wingspan first, then next page: <url> - or last page when next is null. Read the whole input with $input | Out-String.
- First page
- Last page
Lessons teach PowerShell 7; your script runs on PowerShell 6.2 via Try It Online (tio.run), a free public service, so stick to syntax that works there. Test input is piped into your script as $input. Your script and test input are sent to that service.
Who came, who went?
The input is an XML census with two <roster> elements, day="monday" and day="tuesday", each listing <dragon name="..."/> elements.
Use Compare-Object to print arrived: <names> (on Tuesday only), left: <names> (on Monday only) - sorted, comma-separated, or nobody - and unchanged: <n>.
- One in, one out
- Visitors only
Lessons teach PowerShell 7; your script runs on PowerShell 6.2 via Try It Online (tio.run), a free public service, so stick to syntax that works there. Test input is piped into your script as $input. Your script and test input are sent to that service.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…