Lecture 18 - Web APIs and JSON
t3.micro running Ubuntu 26.04ssh -iaptscp and wgetaws command lineSource: K21
Create caseAccount and billing support and explain what happened.env fileopenai library sent the request for us. Today we write the requests ourselvesbase_url is a web address, and the library sends our request there. Today you will see the request and the reply
1. What an API is
2. URLs and status codes
3. JSON
4. APIs from the terminal
curl, a browser for your terminal5. A first taste of Python
curl did. Lecture 19 takes them apart/country/BRA/indicator/SP.POP.TOTL into Brazil’s population, year by yearpandas has an API, and so does your operating systemcurl/country/BRA. Most public data APIs use it/v2/country/{code}/indicator/{code}data/raw/| Kind | What it costs you | Examples |
|---|---|---|
| Open and keyless | Nothing. Just fetch the URL | Open-Meteo, World Bank, National Weather Service, USGS earthquakes, GitHub |
| Free but key-required | A signup form, then a key in every request | NASA, OpenWeatherMap, OpenRouter’s free models, GitHub for higher limits |
| Paid | A key and a bill | OpenRouter’s paid models, most commercial data vendors |
api.github.com/users/danilofreire, fetched with no key at all (click image to enlarge)
https://api.worldbank.org/v2/country/BRA/indicator/NY.GDP.PCAP.KD?format=json&date=2014:2025
| Part | Name | What it does |
|---|---|---|
| https | scheme | Which protocol. Almost always https |
| api.worldbank.org | host | Which machine to ask |
| /v2/country/BRA/indicator/… | path | Which data on that machine |
| ? | separator | Ends the path. Everything after it is the query string |
| format=json&date=2014:2025 | query string | Options, as key=value pairs joined by & |
| Step | Add this | What it means |
|---|---|---|
| 1 | https://api.worldbank.org/v2 |
The API’s base URL, version 2 |
| 2 | /country/BRA |
Brazil, by its three-letter ISO code |
| 3 | /indicator/NY.GDP.PCAP.KD |
GDP per person, in constant US dollars |
| 4 | ?format=json |
Start the options. Send JSON, not XML |
| 5 | &date=2014:2025 |
Only these years |
BRA for URY and you get UruguaySP.POP.TOTL and you get populationNY.GDP.PCAP.KD:
NY = national accounts (income)GDP = gross domestic productPCAP = per capitaKD = constant US dollars?key=value, and & joins them?format=json&date=2014:2025&per_page=100
^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^
option 1 option 2 option 3
?a=1&b=2 and ?b=2&a=1 are the same request?, &, =, #, and the space%20, and a / becomes %2FAmerica/New_York travels as America%2FNew_Yorkrequests library does this for you, so you rarely type it2xx worked, 4xx is your mistake, 5xx is theirs| Code | Name | What it means |
|---|---|---|
200 |
OK | It worked. Go ahead and read the data |
301 / 302 |
Moved | The data lives elsewhere now. Browsers and requests follow these for you |
400 |
Bad Request | Your URL is malformed. Check the query string |
401 |
Unauthorized | You need a key, or yours is wrong |
403 |
Forbidden | Your key works, but it does not give you access to this |
404 |
Not Found | Nothing at that path. Usually a typo |
429 |
Too Many Requests | You are asking too fast. Slow down |
500 |
Internal Server Error | Their problem. Wait and retry |
Some APIs, like the World Bank, answer errors with 200 and hide the failure in the reply. A status check alone is not enough. We will see this in the terminal soon
GET: “please send me this”POST: “here is some data”openai library sent your prompt as a POSTOther methods exist (PUT, DELETE, PATCH), but you rarely need them to read public data
GET and shows the raw replyhttps://api.open-meteo.com/v1/forecastcurrent=temperature_2m) in Atlanta (latitude 33.75, longitude -84.39). You need three parameterslongitude. What happens?&temperature_unit=fahrenheit to the URLWhat to look for
longitudeStuck, or want to compare your URL with mine?
{ } holds "key": value pairs[ ] holds values in orderEach JSON type has a Python equivalent
| JSON | Python |
|---|---|
{...} object |
dict (dictionary) |
[...] array |
list |
"text" |
str (string) |
42, 3.14 |
int, float |
true / false |
True / False |
null |
None |
"sensors" is an array inside an objectShortened from the saved reply, openmeteo_atlanta.json
{ } is an object. It has nine keys, and six are shown herecurrent and current_units, hold objects of their owncurrent. Its unit is inside current_unitstime is text, because JSON has no date typeShortened from wb_gdp_bra.json, the reply to our Brazil URL
[ ], a name picks one from a { }More about the World Bank API reply, and its official documentation: Appendix 07
import json
text = """
{"term": "Fall 2026",
"courses": [
{"code": "DATASCI 350", "enrolled": 40,
"instructor": {"name": "Danilo Freire"}},
{"code": "DATASCI 101", "enrolled": 65,
"instructor": {"name": "Danilo Freire"}}],
"updated": null}
"""
reply = json.loads(text)
print(reply["term"]) # Fall 2026.py file and run itreply["updated"]. What do you see, and why?Stuck, or want to compare your paths with mine?
curl: a browser for your terminalcurl sends a GET to a URL and prints the reply, as one long linesudo apt install curl& and ? as its own symbolsBASE= stores the base URL, and Q= the query string$BASE and $Q paste them back|) the reply into Python’s JSON tool to indent it-s means “silent”: no progress bar-i adds the headers (Appendix 05). The first line is the status| head -n 1 keeps only that lineWithout longitude:
The World Bank answers 200 even for a country that does not exist (XYZ), and puts the error inside the JSON. Check the status and the data
-o means “output”: save the reply to a file instead of printing itdata/ folder were made this waydata/raw/, and read only that fileWhat to look for
Hint for step 3
curl, then load the file:1, newest firstStuck, or want to compare with mine? Appendix 03
requests is the Python library for web APIs. Install it once: pip install requestsimport requests
base = "https://api.open-meteo.com/v1/forecast"
q = "latitude=33.75&longitude=-84.39¤t=temperature_2m"
r = requests.get(base + "?" + q) # send a GET, like curl
print(r.status_code) # 200 means OK
data = r.json() # JSON text -> Python dictionary
print(data["current"]["temperature_2m"]) # the path, one [] per step2xx, 4xx, 5xxparams, and checking them for errorsBefore then
A list of keyless APIs to explore is in Appendix 06
Group names are due by Thursday 5 November
Open-Meteo needs latitude and longitude, and current asks for present conditions
https://api.open-meteo.com/v1/forecast?latitude=33.75
&longitude=-84.39¤t=temperature_2m
Adding a timezone makes the time readable:
https://api.open-meteo.com/v1/forecast?latitude=33.75
&longitude=-84.39¤t=temperature_2m
&timezone=America%2FNew_York
Without longitude, the page shows:
/ in the timezone is written %2F, because / already has a job in a URLtimezone, the time comes back in GMT, which reads oddly for Atlantacourses → 1 → code → “DATASCI 101”courses → 0 → instructor → name → “Danilo Freire”updated → null, which becomes None in PythonThe same paths in Python:
courses is an array, so the second course is position 1, not 2courses, then the course, then instructor, then namenull is JSON’s word for “no value”. It is not the text "null"Part of the output, around 2020:
1 → 5 → value. Records run newest first, so 2025 is position 0 and 2020 is position 5Step 3, in Python:
[1] is the list of records, [5] the 2020 record, and ["value"] its populationdata/wb_pop_ury.jsonGot a FileNotFoundError?
open looks in the folder where you started Python, and it does not understand ~pwd and ls to find the file, then cd there before python3open("/Users/yourname/wb_pop_ury.json")| You write | It travels as |
|---|---|
| space | %20 or + |
/ |
%2F |
? |
%3F |
& |
%26 |
= |
%3D |
: |
%3A |
# |
%23 |
% |
%25 |
% and the character’s byte in hexadecimal (remember them from Lecture 02? 😉)America/New_York becomes America%2FNew_Yorkrequests encodes values for you, so you rarely write these by hand| Header | Purpose |
|---|---|
User-Agent |
Who is asking. Browsers set this, and polite scripts should too |
Accept |
What format you want back (application/json) |
Authorization |
Your API key, for APIs that need one |
Content-Type |
The format of the data being sent |
-I asks for the headers only, without the datacontent-type confirms the reply is JSONmax-age=86400 says the reply can be reused for a day (86,400 seconds)All of these answered a keyless request on 21 August 2026
| API | What it gives you | Documentation |
|---|---|---|
| Open-Meteo | Weather forecasts and history, anywhere | https://open-meteo.com/en/docs |
| World Bank | 29,544 development indicators, all countries | https://datahelpdesk.worldbank.org/knowledgebase/topics/125589-developer-information |
| National Weather Service | US forecasts and alerts, from api.weather.gov/points/{lat},{lon} |
https://www.weather.gov/documentation/services-web-api |
| GitHub | Repositories, users, commits (keyless with low limits) | https://docs.github.com/en/rest |
| Open Library | Books, authors, covers, by ISBN (occasionally flaky) | https://openlibrary.org/developers/api |
| USGS Earthquakes | Every recorded earthquake, live | https://earthquake.usgs.gov/fdsnws/event/1/ |
Pick one this week and fetch something from it, in the browser or with curl
Position 0: the metadata
| Key | Meaning |
|---|---|
page |
Which page this is |
pages |
How many pages there are |
per_page |
Records per page: 50, unless you set per_page |
total |
Records across all pages |
lastupdated |
When the indicator was last updated |
pages is above 1, you only have part of the data&page=2 to the URL, or raise per_pagecountry/all for 2023 returns 266 records, so 6 pages of 50Position 1: one full record from wb_gdp_bra.json
country.id is the two-letter code, countryiso3code the three-letter onedate is text, and value is null (Python’s None) when a year has no data