install.packages(c("httr2", "dplyr", "readr", "tibble", "tidyverse", "maps"))Tracking the ISS from R: A Tutorial on the Open Notify API
1. What is Open Notify and why use it?
Open Notify is a free, open-source project that turns raw NASA data into simple web APIs about space and spacecraft. It has no sign-up, no API key and no cost, which makes it a great first API to learn.
This tutorial uses two of its endpoints:
| Endpoint | What it returns |
|---|---|
/iss-now.json |
The current latitude and longitude of the International Space Station |
/astros.json |
The number of people currently in space, with their names and spacecraft |
Why would someone use it?
- Build a live ISS tracker or map
- Add real space data to a dashboard or class project
- Practice working with a REST API and JSON with no account setup
Why it is a good fit for R: the data arrives as JSON, which R can turn into a tidy data frame, and the location data is easy to plot on a map.
I picked Open Notify because it needs no account or API key, so I could start making calls right away, and I’m deeply interested in astronomy.
2. Setting up the API
On the service’s website
There is nothing to set up. Open Notify does not require an account, an API key or authentication. You can read the documentation at http://open-notify.org/Open-Notify-API/.
Two things from the docs worth knowing:
- Each endpoint is a simple
GETrequest to a URL, for examplehttp://api.open-notify.org/iss-now.json. - The ISS position is only calculated once per second, and the site asks users to poll about once every 5 seconds at most.
In R
Step 1: Install the packages (once).
Step 2: Load them.
library(httr2)
library(dplyr)
library(readr)
library(tibble)Step 3: Build a request. httr2 lets you build a request in pieces. The base URL is the same for every endpoint, and req_url_path() picks which endpoint to call.
base_url <- "http://api.open-notify.org"
req <- request(base_url) |>
req_url_path("iss-now.json")Step 4: Send it and read the JSON.
resp <- req_perform(req)
x <- resp_body_json(resp)
xThe response is a list with a message, a Unix timestamp, and an iss_position containing latitude and longitude.
Why the API calls are not run in this document. API results change every time they run. Following good practice, the calls live in a separate script, fetch_iss_data.R, which saves the results as CSV files. This document then loads those CSVs with read_csv().
3. Demonstration
Call 1: Where is the ISS?
This function (from fetch_iss_data.R) makes one call and turns the response into a one-row data frame. Latitude and longitude come back as text, so they are converted to numbers.
get_iss_position <- function() {
resp <- request(base_url) |>
req_url_path("iss-now.json") |>
req_perform()
x <- resp_body_json(resp)
tibble(
timestamp = as.POSIXct(x$timestamp, origin = "1970-01-01", tz = "UTC"),
latitude = as.numeric(x$iss_position$latitude),
longitude = as.numeric(x$iss_position$longitude)
)
}The script called this function 10 times, 30 seconds apart, and saved the results. Here is the saved data:
iss_track <- read_csv("data/iss_track.csv", show_col_types = FALSE)
kable(iss_track)| timestamp | latitude | longitude |
|---|---|---|
| 2026-11-04 03:10:43 | 18.6664 | 39.0334 |
| 2026-11-04 03:11:13 | 17.1691 | 40.2596 |
| 2026-11-04 03:11:44 | 15.6629 | 41.4640 |
| 2026-11-04 03:12:14 | 14.1488 | 42.6488 |
| 2026-11-04 03:12:45 | 12.6277 | 43.8160 |
| 2026-11-04 03:13:16 | 11.1006 | 44.9678 |
| 2026-11-04 03:13:46 | 9.5683 | 46.1061 |
| 2026-11-04 03:14:17 | 8.0316 | 47.2328 |
| 2026-11-04 03:14:47 | 6.4911 | 48.3501 |
| 2026-11-04 03:15:18 | 4.9477 | 49.4597 |
And here is where those readings fall on a world map:
world <- map_data("world")
ggplot() +
geom_polygon(data = world, aes(long, lat, group = group),
fill = "grey85", color = "white", linewidth = 0.2) +
geom_point(data = iss_track, aes(longitude, latitude, color = timestamp),
size = 3) +
coord_quickmap() +
labs(title = "ISS readings over about 5 minutes",
x = "Longitude", y = "Latitude", color = "Time (UTC)") +
theme_minimal()Call 2: Who is in space right now?
The second endpoint takes no inputs either. The script saved the response to a CSV:
people <- read_csv("data/people_in_space.csv", show_col_types = FALSE)
kable(people |> select(name, craft))| name | craft |
|---|---|
| Oleg Kononenko | ISS |
| Nikolai Chub | ISS |
| Tracy Caldwell Dyson | ISS |
| Matthew Dominick | ISS |
| Michael Barratt | ISS |
| Jeanette Epps | ISS |
| Alexander Grebenkin | ISS |
| Butch Wilmore | ISS |
| Sunita Williams | ISS |
| Li Guangsu | Tiangong |
| Li Cong | Tiangong |
| Ye Guangfu | Tiangong |
Data caveat: The crew list returned by Open Notify may be out of date, so check it against NASA’s website before relying on it. This is a good reminder to verify an API’s data before using it.
A quick summary by spacecraft:
people |> count(craft, name = "people")# A tibble: 2 × 2
craft people
<chr> <int>
1 ISS 9
2 Tiangong 3
4. Wrap-up
- Open Notify is free and needs no API key, so it is a low-friction way to practice calling an API from R.
httr2builds the request,resp_body_json()reads the JSON, andtibble()turns it into a data frame you can analyze or map.- Be polite to the server: keep polling to about once every 5 seconds or slower.
Source: Open Notify, http://open-notify.org (data derived from NASA).