APIs as Objects: `Invoke-RestMethod`

Invoke-RestMethod asks a web API for data and turns its JSON answer into objects, so a weather forecast arrives ready for Select-Object, Where-Object and the rest of the pipeline, and query parameters after a ? in the address ask for exactly what you want.

Journeyman · PowerShell · Updated

After this lesson you can

  • Get a web API's JSON as objects with Invoke-RestMethod, and pick out the data with dots, Select-Object and Where-Object
  • Add query parameters to an API's address with ?, = and &, and keep the whole address in quotes

Key takeaways

  • An API is a web address made for programs. It answers with data, usually JSON, instead of a page.
  • Invoke-RestMethod asks the address and turns the JSON answer into objects, so dots, Select-Object and Where-Object work on it.
  • Query parameters go after ? as name=value, joined by &: ?units=si asks for Celsius.
  • Put the whole address in quotes. Without them, a comma makes it a list and & runs part of it in the background.
  • A saved copy, read with Get-Content -Raw and ConvertFrom-Json, gives the same objects with no internet.

Every morning before you bike to class, you open a weather site, wait for the ads, and scroll past the radar to find one thing: will it rain on the way home? A program can't read that page easily: the numbers are buried among pictures and menus, and the site moves things around whenever it likes. So many services offer a second way in, made for programs. That way in is an API, and PowerShell's Invoke-RestMethod speaks it. Web developers fill their pages from APIs, IT techs pull ticket lists from them, and data analysts collect prices from them every day.

An address for programs: an API

Think of a restaurant. The dining room, with its menus and pictures on the wall, is the website, made for people. The takeout window is the API: you order by the menu's exact name, and you get a labeled box, the same shape every time. API stands for application programming interface: a way for one program to ask another for data.

The US National Weather Service runs a free one at api.weather.gov, with no account and no key. It answers in JSON, the labeled text from JSON: ConvertFrom-Json and ConvertTo-Json.

A forecast takes two questions. First you ask which forecast covers a spot on the map, given as latitude and longitude (here, a park in downtown Chicago):

(Invoke-RestMethod 'https://api.weather.gov/points/41.8826,-87.6233').properties | Select-Object gridId, gridX, gridY, forecast
gridId gridX gridY forecast
------ ----- ----- --------
LOT       76    73 https://api.weather.gov/gridpoints/LOT/76,73/forecast

Read the name as invoke (run) a REST method; REST is the usual style of web API, where every thing has its own address. Invoke-RestMethod asks the address and turns the JSON answer into objects, so the parentheses and the dot work as on any command's answer. Inside properties is the forecast's own address: LOT is the Chicago weather office, and 76,73 a square on its map.

Put the address in quotes. Without them, PowerShell reads the comma in 41.8826,-87.6233 as a list of two things, and stops:

Invoke-RestMethod https://api.weather.gov/points/41.8826,-87.6233
Invoke-RestMethod: Cannot convert 'System.Object[]' to the type 'System.Uri' required by parameter 'Uri'. Specified method is not supported.

The answer is objects

Now ask the forecast address. Its properties hold periods: fourteen forecasts, a day or a night at a time, for the week ahead. Each one is an object, so everything from Module 3 works on it:

(Invoke-RestMethod 'https://api.weather.gov/gridpoints/LOT/76,73/forecast').properties.periods | Select-Object -First 4 name, temperature, shortForecast
name          temperature shortForecast
----          ----------- -------------
Today                  59 Mostly Sunny
Tonight                46 Mostly Clear
Tuesday                59 Sunny
Tuesday Night          44 Mostly Cloudy

Dots go down into the answer one level at a time: properties, then periods. Not sure what a level holds? | Get-Member, from What an Object Is: Get-Member, lists it. Now the question you opened the site for. The chance of rain is a property inside a property, so it goes inside braces:

(Invoke-RestMethod 'https://api.weather.gov/gridpoints/LOT/76,73/forecast').properties.periods | Where-Object { $_.probabilityOfPrecipitation.value -ge 50 } | Select-Object name, temperature, shortForecast
name           temperature shortForecast
----           ----------- -------------
Thursday                46 Rain Showers Likely
Thursday Night          37 Rain Likely
Friday                  44 Rain Likely

And the next few hours in one line, the way a weather radio reads them:

(Invoke-RestMethod 'https://api.weather.gov/gridpoints/LOT/76,73/forecast').properties.periods | Select-Object -First 1 -ExpandProperty detailedForecast
Mostly sunny. High near 59, with temperatures falling to around 55 in the afternoon. West southwest wind 5 to 10 mph.

Your forecast will be today's, not this one. weather.gov covers only the United States; the saved copy at the end of this lesson works anywhere. On a Mac or Linux, every command here is the same.

Asking for more: query parameters

At a takeout window you can say "large, no onions". An address can carry options too, after a question mark. ?units=si asks for the forecast in Celsius and kilometers:

(Invoke-RestMethod 'https://api.weather.gov/gridpoints/LOT/76,73/forecast?units=si').properties.periods | Select-Object -First 4 name, temperature, temperatureUnit
name          temperature temperatureUnit
----          ----------- ---------------
Today                  15 C
Tonight                 8 C
Tuesday                15 C
Tuesday Night           7 C

The forecast address read in parts, like an order at a takeout window: https://api.weather.gov is the place you ask, /gridpoints/LOT/76,73/forecast is what you ask for, ? starts the options, and units=si is one option, a name and a value; & would join another

Read it aloud: ? is "here come the options", and units=si is "units is si" (si is the metric system). Each option, a name, = and a value, is a query parameter; together they're the query string. More are joined with &, read "and". This asks for California's weather alerts of moderate severity:

(Invoke-RestMethod 'https://api.weather.gov/alerts/active?area=CA&severity=Moderate').features.properties.event
Beach Hazards Statement
Dense Fog Advisory
Beach Hazards Statement
Beach Hazards Statement
Beach Hazards Statement
Beach Hazards Statement

Yours will be today's alerts, or nothing on a calm day. Which names an API understands is in its documentation: here, weather.gov/documentation/services-web-api.

Quotes matter even more here. In PowerShell 7, an unquoted & means "run what's before me in the background", so the line breaks in two:

Invoke-RestMethod https://api.weather.gov/alerts/active?area=CA&severity=Moderate
Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
--     ----            -------------   -----         -----------     --------             -------
1      Job1            BackgroundJob   Running       True            localhost            Invoke-RestMethod https:…
severity=Moderate: The term 'severity=Moderate' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

PowerShell started the first half as a background job, then found no command called severity=Moderate. Get-Job | Remove-Job -Force clears the leftover job. Windows PowerShell 5.1 stops instead, saying & is not allowed.

Working without the internet: a saved copy

An answer is only text, so you can keep a copy: for the train with no signal, or to practice on the same data every time. These lines make a practice folder with a copy of the forecast above, cut down to the facts this lesson uses. Running them again is safe.

New-Item -ItemType Directory -Force -Path "$HOME\terminal-practice\weather" | Out-Null
Set-Content -Path "$HOME\terminal-practice\weather\forecast.json" -Value '{ "properties": { "periods": [
  {"name":"Today","temperature":59,"probabilityOfPrecipitation":{"value":0},"shortForecast":"Mostly Sunny","detailedForecast":"Mostly sunny. High near 59, with temperatures falling to around 55 in the afternoon. West southwest wind 5 to 10 mph."},
  {"name":"Tonight","temperature":46,"probabilityOfPrecipitation":{"value":0},"shortForecast":"Mostly Clear"},
  {"name":"Tuesday","temperature":59,"probabilityOfPrecipitation":{"value":0},"shortForecast":"Sunny"},
  {"name":"Tuesday Night","temperature":44,"probabilityOfPrecipitation":{"value":0},"shortForecast":"Mostly Cloudy"},
  {"name":"Veterans Day","temperature":54,"probabilityOfPrecipitation":{"value":40},"shortForecast":"Mostly Cloudy then Chance Rain Showers"},
  {"name":"Wednesday Night","temperature":43,"probabilityOfPrecipitation":{"value":45},"shortForecast":"Chance Rain Showers"},
  {"name":"Thursday","temperature":46,"probabilityOfPrecipitation":{"value":69},"shortForecast":"Rain Showers Likely"},
  {"name":"Thursday Night","temperature":37,"probabilityOfPrecipitation":{"value":65},"shortForecast":"Rain Likely"},
  {"name":"Friday","temperature":44,"probabilityOfPrecipitation":{"value":56},"shortForecast":"Rain Likely"},
  {"name":"Friday Night","temperature":35,"probabilityOfPrecipitation":{"value":23},"shortForecast":"Slight Chance Rain"},
  {"name":"Saturday","temperature":48,"probabilityOfPrecipitation":{"value":10},"shortForecast":"Mostly Sunny"},
  {"name":"Saturday Night","temperature":38,"probabilityOfPrecipitation":{"value":2},"shortForecast":"Mostly Clear"},
  {"name":"Sunday","temperature":52,"probabilityOfPrecipitation":{"value":1},"shortForecast":"Sunny"},
  {"name":"Sunday Night","temperature":43,"probabilityOfPrecipitation":{"value":2},"shortForecast":"Mostly Clear"}
] } }'

They print nothing when they work. Read the copy back with Get-Content -Raw, the whole file as one piece of text, and ConvertFrom-Json, and you get the same objects:

Set-Location "$HOME\terminal-practice\weather"
(Get-Content forecast.json -Raw | ConvertFrom-Json).properties.periods | Select-Object -First 4 name, temperature, shortForecast
name          temperature shortForecast
----          ----------- -------------
Today                  59 Mostly Sunny
Tonight                46 Mostly Clear
Tuesday                59 Sunny
Tuesday Night          44 Mostly Cloudy

That's all Invoke-RestMethod did: fetch the JSON and convert it. Pages and Downloads: Invoke-WebRequest shows how to save a fresh copy. And an API is someone else's computer: ask once and work with the answer, rather than asking over and over.

Try it. Get the forecast for a town you know in the US: find its latitude and longitude (a map app shows them when you right-click or hold a spot), ask points for its forecast address, then list the periods with a chance of rain of 30 or more.

Check yourself

2 questions to try on the lesson page.

So what

A website is for reading; an API is for asking. With Invoke-RestMethod, the answer arrives as objects, and everything you know about the pipeline works on it.

Key terms

Cheat sheets

Related