Computing FoundationsAPIs and the Web › Day 22

Hands-on lab — Day 22: What an API Is and Why Everything Has One

Commands

Setup

cd labs/sections/computing-foundations/day-022-what-an-api-is-and-why

Run

bash examples/call_apis.sh
bash starter/call_apis.sh

Test

bash tests/run_tests.sh

File tree

examples/call_apis.sh
expected-output/sample-run.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/api-worksheet.md
starter/call_apis.sh
tests/run_tests.sh
troubleshooting.md

Lab README

Day 022 lab — Call Your First APIs

Lesson

  • Lesson title: What an API Is and Why Everything Has One
  • Day number: 22 of 365
  • Lesson article: https://ai-roadmap-365.github.io/day-022-what-an-api-is-and-why
  • Lab files: everything you need is in this directory — follow “How to run” below.
  • Browse the course locally: from the repository root, this lab also appears in the course website at /labs/day-022-what-an-api-is-and-why when the site is running.

Purpose

Day 22's lesson explained what an API is: a contract two programs agree on so one can ask the other to do work. This lab makes it real. You call three free, public, no-key web APIs from the terminal, read their JSON responses, and see for yourself that an API call is just an HTTP request to an endpoint that comes back as structured data — the same move that later reaches a hosted model.

Learning objectives

  • Call a web API with curl and read its JSON response.
  • Fetch two different resources (/todos/1, /users/1) from one API and see how the endpoint selects what comes back.
  • Send a query parameter and watch httpbin echo it back in an args object.
  • Read live data (the space station's current position) and tell it apart from fixed sample data.
  • Pretty-print JSON with python3 -m json.tool (no jq required).

Prerequisites

  • The Day 22 lesson, and days 15–21 (the networking category).
  • Day 21 comfort with curl.
  • A terminal with curl and python3, and an internet connection.

Supported operating systems

  • macOS and Linux — fully supported (curl and python3 preinstalled).
  • Windowscurl ships with Windows 10+ and python3 is a free install; or run everything unmodified inside WSL.

Hardware requirements

Any computer with an internet connection. The lab only makes ordinary web requests and reads the responses.

Required software

  • curl — to send the requests (preinstalled on macOS/Linux).
  • python3 — only to pretty-print JSON via python3 -m json.tool (preinstalled on macOS/Linux). jq is optional, never required.

See requirements/README.md.

Free and open-source options

Everything here is free: curl and Python are open source, and all three test APIs — JSONPlaceholder, httpbin, and Open Notify — are free public endpoints with no account and no key.

Installation

None. Change into this directory:

cd labs/sections/computing-foundations/day-022-what-an-api-is-and-why

File structure

day-022-what-an-api-is-and-why/
├── README.md
├── metadata.yml
├── starter/
│   ├── call_apis.sh          ← YOUR working file (4 exercises)
│   └── api-worksheet.md      ← record the values you got back
├── examples/
│   └── call_apis.sh          ← completed reference implementation
├── tests/
│   └── run_tests.sh
├── expected-output/
│   └── sample-run.txt        ← a real captured run
├── requirements/README.md
├── troubleshooting.md
└── security.md

How to run

## 1. See the finished tool first (needs network)
bash examples/call_apis.sh

## 2. Complete the four exercises in the starter, then run it
bash starter/call_apis.sh

## 3. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/call_apis.sh — makes four API calls and explains each: JSONPlaceholder /todos/1 and /users/1, httpbin /get with a query parameter, and Open Notify's live iss-now.json. It pretty-prints each JSON response with python3 -m json.tool, and if a service returns something that is not JSON it prints the raw body instead of crashing. It degrades gracefully offline (clear message, exit 0).
  • bash starter/call_apis.sh — the same skeleton with four numbered exercises; each names the exact curl command to run and what to record.
  • bash tests/run_tests.sh — always verifies structure (files present, both scripts parse, the example calls all three APIs and does not require jq, the starter names four exercises, python3 is available), then — when online — checks that each API returns parseable JSON with its documented key (title, email, args, iss_position). Offline or during a transient outage, the affected live checks are skipped (never failed). Exits 0 when no structure check fails.

Expected output

See expected-output/sample-run.txt for a real captured run. The JSONPlaceholder data is fixed sample data, but the ISS latitude/longitude and timestamp are live, so yours will differ.

Validation steps

  1. bash examples/call_apis.sh prints all four sections without errors.
  2. You can point at each response and identify it as JSON with named fields.
  3. You found your course and day parameters echoed by httpbin's args.
  4. You ran the ISS call twice and saw the coordinates change.
  5. The tests pass.

Tests

bash tests/run_tests.sh

Expected final line online: 17 checks, 0 failure(s), 0 skip(s). (a transiently down service turns its check into a skip, never a failure; offline, all four live checks skip). Exit 0 on success.

Cleanup

Nothing to clean up — the scripts only make web requests and write no files. Reset your edited starter with git checkout -- starter/call_apis.sh.

Troubleshooting

See troubleshooting.md — no network, unindented JSON, httpbin 503s, the ISS http-vs-https gotcha, and shell-quoting URLs with &.

Security notes

See security.md. Short version: only public, no-auth endpoints; never send secrets to an echo service; an API key is a password.

Extension exercises

  1. Filter with a parameter: curl -s "https://jsonplaceholder.typicode.com/todos?userId=1" | python3 -m json.tool returns only user 1's to-dos. Count how many came back.
  2. Add response headers with curl -i https://jsonplaceholder.typicode.com/todos/1 and find the Content-Type: application/json header.
  3. Time a call with curl -o /dev/null -s -w 'status:%{http_code} total:%{time_total}s\n' https://jsonplaceholder.typicode.com/todos/1 and compare it with how long a local calculation takes.
  • Previous day: Day 21 — Inspecting Traffic with curl and Developer Tools.
  • Next day: Day 23 — REST Fundamentals: Resources and Verbs.

Expected output

sample-run.txt

Day 022 — Call Your First APIs
Three free public APIs, no key required.

=== 1. JSONPlaceholder — GET /todos/1 (a single to-do item) ===
$ curl https://jsonplaceholder.typicode.com/todos/1
{
    "userId": 1,
    "id": 1,
    "title": "delectus aut autem",
    "completed": false
}
Explanation: a GET to the /todos/1 endpoint returns one to-do as JSON.
The 'title' field is the to-do's text; 'completed' is true/false.

=== 2. JSONPlaceholder — GET /users/1 (a user, with nested objects) ===
$ curl https://jsonplaceholder.typicode.com/users/1
{
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
        "street": "Kulas Light",
        "suite": "Apt. 556",
        "city": "Gwenborough",
        "zipcode": "92998-3874",
        "geo": {
            "lat": "-37.3159",
            "lng": "81.1496"
        }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
        "name": "Romaguera-Crona",
        "catchPhrase": "Multi-layered client-server neural-net",
        "bs": "harness real-time e-markets"
    }
}
Explanation: same API, different endpoint -> different resource.
Note the nested 'address' and 'company' objects — JSON can nest.

=== 3. httpbin — GET /get?course=365-days-of-ai&day=22 (echoes your request) ===
$ curl "https://httpbin.org/get?course=365-days-of-ai&day=22"
{
    "args": {
        "course": "365-days-of-ai",
        "day": "22"
    },
    "headers": {
        "Accept": "*/*",
        "Host": "httpbin.org",
        "User-Agent": "curl/8.7.1",
        "X-Amzn-Trace-Id": "Root=1-6a538d93-395badf86c29f8ab41f84473"
    },
    "origin": "122.170.198.151",
    "url": "https://httpbin.org/get?course=365-days-of-ai&day=22"
}
Explanation: the 'args' object shows the query parameters the server saw
(course, day); the 'headers' object shows the headers curl sent.

=== 4. Open Notify — GET iss-now.json (LIVE space-station position) ===
$ curl http://api.open-notify.org/iss-now.json
{
    "timestamp": 1783860628,
    "iss_position": {
        "latitude": "36.1156",
        "longitude": "86.5516"
    },
    "message": "success"
}
Explanation: 'iss_position' holds the latitude/longitude RIGHT NOW.
Run again in a minute and the numbers change — a real satellite is moving.
Note: this endpoint is served over plain http://, not https://.

Done. Each section above was one API call: a request to an endpoint and
a JSON response you read fields out of.

Source files

examples/call_apis.sh (5076 bytes)
#!/usr/bin/env bash
# Day 022 lab — Call Your First APIs (completed reference implementation).
#
# Calls three free, public, no-key web APIs and explains each response:
#   * JSONPlaceholder (https://jsonplaceholder.typicode.com) — fake REST data
#   * httpbin        (https://httpbin.org)                    — echoes your request
#   * Open Notify    (http://api.open-notify.org)             — live ISS position
#
# JSON is pretty-printed with `python3 -m json.tool`, which is always available
# on macOS and Linux — jq is NOT required. If a call returns something that is
# not JSON (an outage page, say), we print the raw body instead of crashing.
#
# Run from the lab directory:  bash examples/call_apis.sh
#
# Requires network access. Offline (or if a service is transiently down), each
# section degrades gracefully with a clear message and the script still exits 0.
set -u

JSONPH="https://jsonplaceholder.typicode.com"
HTTPBIN="https://httpbin.org"
ISS="http://api.open-notify.org/iss-now.json"

rule() { printf '\n=== %s ===\n' "$1"; }

# Pretty-print stdin as JSON, or fall back to printing it raw if it is not JSON.
pretty() {
  local body; body="$(cat)"
  if printf '%s' "${body}" | python3 -m json.tool 2>/dev/null; then
    return 0
  fi
  echo "(response was not valid JSON — showing raw body / message):"
  printf '%s\n' "${body}"
}

# Return 0 if we appear to have network access, else 1.
have_network() {
  curl -s -o /dev/null --max-time 12 "${JSONPH}/todos/1" 2>/dev/null
}

# GET a URL with sane timeouts/retries; prints the body, or empty on failure.
fetch() {
  curl -s --max-time 25 --retry 3 --retry-delay 2 "$1" 2>/dev/null
}

# ---------------------------------------------------------------------------
# 1. JSONPlaceholder — a single to-do resource at /todos/1.
# ---------------------------------------------------------------------------
show_todo() {
  rule "1. JSONPlaceholder — GET /todos/1 (a single to-do item)"
  echo "\$ curl ${JSONPH}/todos/1"
  local body; body="$(fetch "${JSONPH}/todos/1")"
  if [ -z "${body}" ]; then echo "(could not reach ${JSONPH})"; return; fi
  printf '%s' "${body}" | pretty
  echo "Explanation: a GET to the /todos/1 endpoint returns one to-do as JSON."
  echo "The 'title' field is the to-do's text; 'completed' is true/false."
}

# ---------------------------------------------------------------------------
# 2. JSONPlaceholder — a single user resource at /users/1 (nested JSON).
# ---------------------------------------------------------------------------
show_user() {
  rule "2. JSONPlaceholder — GET /users/1 (a user, with nested objects)"
  echo "\$ curl ${JSONPH}/users/1"
  local body; body="$(fetch "${JSONPH}/users/1")"
  if [ -z "${body}" ]; then echo "(could not reach ${JSONPH})"; return; fi
  printf '%s' "${body}" | pretty
  echo "Explanation: same API, different endpoint -> different resource."
  echo "Note the nested 'address' and 'company' objects — JSON can nest."
}

# ---------------------------------------------------------------------------
# 3. httpbin — /get echoes back the request it received.
# ---------------------------------------------------------------------------
show_httpbin() {
  rule "3. httpbin — GET /get?course=365-days-of-ai&day=22 (echoes your request)"
  echo "\$ curl \"${HTTPBIN}/get?course=365-days-of-ai&day=22\""
  local body; body="$(fetch "${HTTPBIN}/get?course=365-days-of-ai&day=22")"
  if [ -z "${body}" ]; then
    echo "(could not reach ${HTTPBIN} — it is a shared free service and is"
    echo " sometimes overloaded; wait a minute and re-run. This is not your bug.)"
    return
  fi
  printf '%s' "${body}" | pretty
  echo "Explanation: the 'args' object shows the query parameters the server saw"
  echo "(course, day); the 'headers' object shows the headers curl sent."
}

# ---------------------------------------------------------------------------
# 4. Open Notify — the current position of the International Space Station.
# ---------------------------------------------------------------------------
show_iss() {
  rule "4. Open Notify — GET iss-now.json (LIVE space-station position)"
  echo "\$ curl ${ISS}"
  local body; body="$(fetch "${ISS}")"
  if [ -z "${body}" ]; then echo "(could not reach ${ISS})"; return; fi
  printf '%s' "${body}" | pretty
  echo "Explanation: 'iss_position' holds the latitude/longitude RIGHT NOW."
  echo "Run again in a minute and the numbers change — a real satellite is moving."
  echo "Note: this endpoint is served over plain http://, not https://."
}

main() {
  echo "Day 022 — Call Your First APIs"
  echo "Three free public APIs, no key required."
  if ! have_network; then
    echo
    echo "OFFLINE: could not reach ${JSONPH}. Every call below needs network"
    echo "access. Connect to the internet and re-run: bash examples/call_apis.sh"
    exit 0
  fi
  show_todo
  show_user
  show_httpbin
  show_iss
  echo
  echo "Done. Each section above was one API call: a request to an endpoint and"
  echo "a JSON response you read fields out of."
}

main "$@"
metadata.yml (576 bytes)
lesson_id: D022
day: 22
kind: api-example
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-022-what-an-api-is-and-why
run_commands:
  - bash examples/call_apis.sh
  - bash starter/call_apis.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/call_apis.sh  # optional: reset your work'
requires_network: true
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (curl 8.7.1, Python 3.14.0), online, bash tests/run_tests.sh → 17 checks, 0 failure(s), 0 skip(s)'
requirements/README.md (822 bytes)
# Dependencies — Day 022 lab

**`curl` and `python3` only** — both preinstalled on macOS and mainstream
Linux (and available on Windows 10+ / WSL). No package installs, no accounts,
no API keys.

- `curl` sends the HTTP requests.
- `python3 -m json.tool` pretty-prints the JSON responses. It is part of the
  Python standard library, so nothing extra is needed.
- `jq` is **optional**. Some people like it for JSON, but this lab deliberately
  does **not** require it, so the scripts run on a stock machine.

The lab reaches three free public APIs — `jsonplaceholder.typicode.com`,
`httpbin.org`, and `api.open-notify.org` — so it needs network access to show
live output. With no network it degrades gracefully: the scripts print a clear
message and the tests skip (never fail) the live checks. No sudo required.
starter/api-worksheet.md (1416 bytes)
# API worksheet — Day 022

Call the three public APIs (from the examples or your completed starter) and
record what YOU got back. Some values are fixed; the ISS position is live, so
yours will differ from anyone else's — that is the point.

## 1. JSONPlaceholder — a to-do item

- Command: `curl -s https://jsonplaceholder.typicode.com/todos/1 | python3 -m json.tool`
- The `title` of to-do #1: `____________________________________`
- Is it `completed`?  (true / false): `______`

## 2. JSONPlaceholder — a user

- Command: `curl -s https://jsonplaceholder.typicode.com/users/1 | python3 -m json.tool`
- Pick ONE field and record it:
  - Field name: `____________`  Value: `____________________________`

## 3. httpbin — echo your request

- Command: `curl -s "https://httpbin.org/get?course=365-days-of-ai&day=22" | python3 -m json.tool`
- What appeared inside the `"args"` object? `____________________________`

## 4. Open Notify — live ISS position

- Command: `curl -s http://api.open-notify.org/iss-now.json | python3 -m json.tool`
- Latitude right now: `____________`   Longitude right now: `____________`
- Roughly what time did you run it? `____________`

## 5. One short paragraph

Using the restaurant analogy, name the menu, the order, and the plate that came
back for one of these calls. Then say which of the three responses was **live
data** and how you could tell.

> _your answer here_
starter/call_apis.sh (2020 bytes)
#!/usr/bin/env bash
# Day 022 starter — Call Your First APIs.
# Complete the four exercises below. Each names the exact curl command to run.
# Run from the lab directory:  bash starter/call_apis.sh
# The completed reference is in ../examples/call_apis.sh — try it yourself first.
set -u

JSONPH="https://jsonplaceholder.typicode.com"
HTTPBIN="https://httpbin.org"
ISS="http://api.open-notify.org/iss-now.json"
rule() { printf '\n=== %s ===\n' "$1"; }

echo "Day 022 starter — Call Your First APIs"
echo "Tip: pipe any command through '| python3 -m json.tool' to pretty-print JSON."

# Exercise 1: fetch a single to-do from JSONPlaceholder.
# Replace the echo with:
#   curl -s "$JSONPH/todos/1" | python3 -m json.tool
rule "1. JSONPlaceholder — GET /todos/1"
echo "EXERCISE — your turn: GET $JSONPH/todos/1 and pretty-print the JSON."
echo "Record the 'title' field on the worksheet."

# Exercise 2: fetch a single user from JSONPlaceholder.
# Replace with:
#   curl -s "$JSONPH/users/1" | python3 -m json.tool
rule "2. JSONPlaceholder — GET /users/1"
echo "EXERCISE — your turn: GET $JSONPH/users/1 and pretty-print the JSON."
echo "Record ONE field of your choice (e.g. 'name' or 'email') on the worksheet."

# Exercise 3: echo your request with httpbin, including a query parameter.
# Replace with:
#   curl -s "$HTTPBIN/get?course=365-days-of-ai&day=22" | python3 -m json.tool
rule "3. httpbin — GET /get with a query parameter"
echo "EXERCISE — your turn: GET $HTTPBIN/get?course=365-days-of-ai&day=22"
echo "Find your 'course' and 'day' in the echoed 'args' object."

# Exercise 4: read the LIVE position of the space station from Open Notify.
# Replace with:
#   curl -s "$ISS" | python3 -m json.tool
rule "4. Open Notify — GET iss-now.json (live ISS position)"
echo "EXERCISE — your turn: GET $ISS  (note: plain http://, not https://)"
echo "Record the current latitude and longitude on the worksheet."

echo
echo "When all four are done, compare your output with ../examples/call_apis.sh"
tests/run_tests.sh (5555 bytes)
#!/usr/bin/env bash
# Tests for the Day 022 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Two kinds of checks:
#   * Structure checks always run and must pass (files present, the example
#     script calls each API, the starter names its four exercises, both scripts
#     parse, python3 is available for pretty-printing).
#   * Network checks run only when the internet is reachable. Offline, they are
#     SKIPPED with a message. When online but a public service is transiently
#     unavailable, the affected check is also SKIPPED rather than failed — an
#     external outage is not the learner's bug.
#
# The script exits 0 when no structure check failed, whether online or offline.
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
JSONPH="https://jsonplaceholder.typicode.com"
HTTPBIN="https://httpbin.org"
ISS="http://api.open-notify.org/iss-now.json"
failures=0
checks=0
skips=0

pass() { checks=$((checks + 1)); echo "  ok:   $1"; }
fail() { checks=$((checks + 1)); failures=$((failures + 1)); echo "  FAIL: $1"; }
skip() { skips=$((skips + 1)); echo "  skip: $1"; }

# Fetch a URL; echo the body on success, nothing on failure.
fetch() { curl -s --max-time 25 --retry 3 --retry-delay 2 "$1" 2>/dev/null; }
# True if the body parses as JSON.
is_json() { printf '%s' "$1" | python3 -m json.tool >/dev/null 2>&1; }
# True if the JSON body contains a top-level (or nested) key name.
has_key() { printf '%s' "$1" | python3 -c "import sys,json; d=json.dumps(json.load(sys.stdin)); sys.exit(0 if '\"$2\"' in d else 1)" 2>/dev/null; }

echo "== Structure checks =="
example="${lab_dir}/examples/call_apis.sh"
starter="${lab_dir}/starter/call_apis.sh"

[ -f "${example}" ] && pass "example script exists" || fail "example script missing"
[ -f "${starter}" ] && pass "starter script exists" || fail "starter script missing"
[ -f "${lab_dir}/starter/api-worksheet.md" ] && pass "worksheet exists" || fail "worksheet missing"

# python3 must be available (used to pretty-print without requiring jq).
if command -v python3 >/dev/null 2>&1; then pass "python3 available for JSON pretty-printing"; else fail "python3 not found"; fi

# The example must call each of the three APIs and NOT hard-require jq.
for needle in "jsonplaceholder.typicode.com" "httpbin.org" "api.open-notify.org" "python3 -m json.tool"; do
  if grep -q -- "${needle}" "${example}"; then pass "example uses '${needle}'"; else fail "example missing '${needle}'"; fi
done
if grep -qE '\| *jq' "${example}"; then fail "example should not pipe to jq (use python3 -m json.tool)"; else pass "example does not require jq (uses python3)"; fi

# The starter must name its four numbered exercises.
ex_count="$(grep -cE 'Exercise [1-4]' "${starter}" 2>/dev/null || true)"
if [ "${ex_count:-0}" -ge 4 ]; then pass "starter has 4 numbered exercises"; else fail "starter has 4 numbered exercises (found ${ex_count:-0})"; fi

# Both scripts must be valid bash.
if bash -n "${example}" 2>/dev/null; then pass "example has valid bash syntax"; else fail "example has a syntax error"; fi
if bash -n "${starter}" 2>/dev/null; then pass "starter has valid bash syntax"; else fail "starter has a syntax error"; fi

echo
echo "== Network checks =="
if ! curl -s -o /dev/null --max-time 12 "${JSONPH}/todos/1" 2>/dev/null; then
  skip "no network access — skipping all live-API checks (expected offline)"
else
  pass "network reachable (JSONPlaceholder responded)"

  # A network check only FAILS when a service returns valid JSON that is
  # missing the documented key (a real contract break). If the service is
  # unreachable or returns a non-JSON outage page, that is a transient outage,
  # not the learner's bug, so we SKIP.

  # Check 1: JSONPlaceholder /todos/1 returns JSON with the 'title' key.
  body="$(fetch "${JSONPH}/todos/1")"
  if [ -z "${body}" ] || ! is_json "${body}"; then skip "todos/1 — JSONPlaceholder transiently unavailable; retry later"
  elif has_key "${body}" "title"; then pass "GET /todos/1 returned JSON with a 'title' key"
  else fail "GET /todos/1 returned JSON but without the documented 'title' key"; fi

  # Check 2: JSONPlaceholder /users/1 returns JSON with the 'email' key.
  body="$(fetch "${JSONPH}/users/1")"
  if [ -z "${body}" ] || ! is_json "${body}"; then skip "users/1 — JSONPlaceholder transiently unavailable; retry later"
  elif has_key "${body}" "email"; then pass "GET /users/1 returned JSON with an 'email' key"
  else fail "GET /users/1 returned JSON but without the documented 'email' key"; fi

  # Check 3: httpbin /get echoes the query parameters in an 'args' object.
  body="$(fetch "${HTTPBIN}/get?course=365-days-of-ai&day=22")"
  if [ -z "${body}" ] || ! is_json "${body}"; then skip "httpbin /get — shared service transiently unavailable; retry later"
  elif has_key "${body}" "args" && printf '%s' "${body}" | grep -q "365-days-of-ai"; then
    pass "GET /get echoed the query parameters in 'args'"
  else fail "GET /get returned JSON but did not echo the query parameters in 'args'"; fi

  # Check 4: Open Notify ISS returns JSON with an 'iss_position' key.
  body="$(fetch "${ISS}")"
  if [ -z "${body}" ] || ! is_json "${body}"; then skip "ISS — Open Notify transiently unavailable; retry later"
  elif has_key "${body}" "iss_position"; then pass "GET iss-now.json returned JSON with 'iss_position'"
  else fail "GET iss-now.json returned JSON but without the documented 'iss_position' key"; fi
fi

echo
echo "${checks} checks, ${failures} failure(s), ${skips} skip(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 022 lab

curl: (6) Could not resolve host

Your machine cannot reach the internet — DNS failed (Day 16). Check your connection and try again; every call in this lab needs network access. The scripts and tests detect no network and exit 0 with a clear message.

The JSON prints as one long unindented line

That is the raw response body. Pipe it through python3 -m json.tool to pretty-print it with indentation:

curl -s https://jsonplaceholder.typicode.com/todos/1 | python3 -m json.tool

If Python then reports Expecting value: line 1 column 1, the API returned something that is not JSON (usually a transient error page) — retry in a moment, and check the URL for a typo.

httpbin.org times out or returns a 503

httpbin.org is a shared free service and is sometimes overloaded. The example retries transient failures and falls back to a clear message; the test turns a transient httpbin outage into a skip, never a failure. Wait a minute and re-run — the other two APIs are unaffected. A 503 is itself a real, valid server response worth recognizing.

The ISS command "fails" only when I add https

Open Notify's ISS endpoint is served over plain http://, not https://. Use the URL exactly as written: http://api.open-notify.org/iss-now.json. This is a good reminder that not every service offers TLS (Day 19).

The httpbin URL breaks in my shell

A URL containing & must be quoted, or the shell reads & as "run in the background" and mangles the request:

curl -s "https://httpbin.org/get?course=365-days-of-ai&day=22"   # quotes required

python3: command not found

On some minimal Linux setups Python 3 is installed as python. Try python -m json.tool, or install python3 from your package manager (Day 13). On macOS python3 is present by default.

Offline

The scripts and tests detect no network and exit 0 with a message. Connect to the internet to see live output.

Security notes

Security notes — Day 022 lab

  • What it does: sends ordinary GET requests to three free, public, no-authentication test APIs (jsonplaceholder.typicode.com, httpbin.org, api.open-notify.org) and reads the responses. No data of yours is sent beyond a normal request; no sudo; no files written outside the lab.
  • Public, no-auth APIs only. These three need no key on purpose. Do not add authentication where none is asked for, and never invent or paste a key.
  • An API key is a secret, like a password. Real APIs (next week) require a key or token in a header. Whoever holds your key can call the API as you, on your bill. Never commit a key to a repository, paste it into a screenshot or forum, or put it in a shared URL.
  • Never send secrets to an echo service. httpbin.org reflects your request back to you, so anything you send is visible in the response. This lab sends only harmless sample values (course, day). Do not send real tokens, passwords, or personal data to any echo service.
  • Every API call transmits data to someone else's computer. For these demos that is nothing sensitive, but the habit to build now is to know exactly what each request sends before you send it.
  • Only call services you are allowed to. These three publish open APIs for exactly this use. Probing or hammering servers you do not own or have permission to test can violate their terms or the law; the retry limits in these scripts are deliberately gentle.