# 14.6 JSON and REST APIs (/building-ai-workers/structuring-programs/json-and-rest-apis)

---
type: Document
title: "14.6 JSON and REST APIs"
description: "How to read a request to another program's API and the JSON that comes back, act on each status code on purpose, fetch every page and keep the key out of the code."
status: stable
order: 314.06
ksor:
  owner: team:panaversity
  audience: [ public ]
  approval:
    by: process:panaversity
    at: 2026-11-10T15:33:14Z
chapter: "14"
part: III
expert_status: builder-only
concepts: [ "14.6" ]
last_verified: 2026-10-30
sources:
  - id: node-globals
    title: "Globals (Node.js, accessed 2026-10-30)"
    resource: https://nodejs.org/api/globals.html
  - id: httpx-async
    title: "Async support (httpx, accessed 2026-10-30)"
    resource: https://www.python-httpx.org/async/
generated:
  at: 2026-11-10T15:33:14Z
  by: process:panaversity
trust_tier: unverified
build_id: sha256:46fd53c5d7ab1c14f6347908d45c9fdaecda4d914439923ee878bdcecd57803e
dirty: true
ksor_version: 0.0.60
---

**In everyday life.** A takeout window has a menu taped beside it. You ask for item 12 with no onions, and you get a bag with a receipt. You never see the kitchen. So you check the bag before you leave, because the kitchen can change a recipe without telling you.

**What you will learn.** How to read a request to another program and its answer, and check what arrives before you use it.

**Why it matters.** The other program belongs to someone else, who can change it without telling you. So your program checks every answer before it uses it.

### What is JSON?

**JSON** is the text format most programs use to send records to each other. `{ }` holds a dictionary and `[ ]` a list, text goes in quotes, and numbers, `true`, `false` and `null` are written plainly. JSON has no dates, so a date travels as text, as in `{"number": "INV-8306", "dueDate": "2027-03-08"}`.

### What are the parts of a request?

An **API**, an application programming interface, is a way for one program to ask another for something. A **REST API** does it over the web, with one address, or **URL**, for each kind of thing. Each of those addresses is an **endpoint**. At the takeout window, the menu is the list of endpoints, your order is the request, and the bag with its receipt is the response.

| Part of a request or response | What it is | Brightline sample API |
| --- | --- | --- |
| Method | What you want to do: `GET` reads, `POST` creates | `GET` |
| Endpoint | The address of the thing | `/invoices` |
| Query string | Options after a `?`, joined by `&` | `?status=open&page=2` |
| **Header** | A labeled extra on the request or response | `X-API-Key: ...`, `Content-Type: application/json` |
| **Status code** | A number that says how it went | `200`, `401`, `503` |
| Body | The data, usually JSON | `{"invoices": [...], "page": 2, "pages": 3}` |

**Pages.** An API rarely returns thousands of records at once. It returns a **page** of them and tells you how many pages there are. A common bug in a **client**, the program that sends the requests, is reading only the first page. It gives no warning.

**At work.** Brightline's repository includes a small sample REST API that runs on your own computer. It needs the key from `.env` and serves five invoices per page.

Here is one order at the window, and what comes back. `localhost` in the address means your own computer. The key comes from the environment, as [Where does the key go?](#where-does-the-key-go) explains. Predict each line.

```ts tab="TypeScript" tab-group="language"
// the order: GET /invoices, the open ones, page 2, with the key in a header
const response = await fetch("http://localhost:4010/invoices?status=open&page=2", {
  headers: { "X-API-Key": process.env.AP_API_KEY ?? "" },
});
// the receipt: how it went, and what kind of body came back
console.log(response.status, response.headers.get("content-type"));
// the bag: the body, read as JSON
const body = await response.json();
console.log(body.page, body.pages, body.invoices.length, body.invoices[0].number);
// a record of your own, turned into JSON text
console.log(JSON.stringify({ number: "INV-8306", amountCents: 158200, dueDate: "2027-03-08" }));
```

```python tab="Python" tab-group="language"
import json
import os

import httpx

# the order: GET /invoices, the open ones, page 2, with the key in a header
response = httpx.get(
    "http://localhost:4010/invoices",
    params={"status": "open", "page": 2},
    headers={"X-API-Key": os.environ["AP_API_KEY"]},
)
# the receipt: how it went, and what kind of body came back
print(response.status_code, response.headers["content-type"])
# the bag: the body, read as JSON
body = response.json()
print(body["page"], body["pages"], len(body["invoices"]), body["invoices"][0]["number"])
# a record of your own, turned into JSON text
print(json.dumps({"number": "INV-8306", "amountCents": 158200, "dueDate": "2027-03-08"}))
```

| Language | The marks in this code |
| --- | --- |
| TypeScript | `fetch` makes the request, and `await` waits for the answer, which [14.7](async-and-await.md) explains. `process.env.AP_API_KEY` reads the key from the environment, and `?? ""` uses empty text if there is none. `JSON.stringify` turns a record into JSON text. |
| Python | httpx is the library this book uses to send web requests, and its `params` builds the query string. `os.environ["AP_API_KEY"]` reads the key from the environment. `json.dumps` turns a record into JSON text. |

```text tab="TypeScript" tab-group="language"
200 application/json
2 3 5 INV-8306
{"number":"INV-8306","amountCents":158200,"dueDate":"2027-03-08"}
```

```text tab="Python" tab-group="language"
200 application/json
2 3 5 INV-8306
{"number": "INV-8306", "amountCents": 158200, "dueDate": "2027-03-08"}
```

### What should the client do for each status code?

Act on each one on purpose. Neither `fetch` nor `httpx.get` raises an error for a `401` or a `503`. Each hands back the answer with its code, so a program that ignores the code reads an error message as if it were invoices.

| Code | Means | What the client should do |
| --- | --- | --- |
| 200 | OK | Read the body |
| 400 | Bad request: your request was wrong | Fix the request. Retrying will not help |
| 401 | Not authorized: no key, or the wrong key | Check the key. Retrying will not help |
| 404 | Not found | Check the endpoint |
| 429 | Too many requests | Wait, then try again |
| 503 | The server is busy or down for a moment | Wait briefly and retry |

### Why did the tests pass and the live run fail?

Because the tests and the code shared one picture of the data, and the API had its own. Only the live answer shows what the API really sends, and the schema at the [edge](data-models-and-schemas.md#why-check-at-the-edge) reads it.

**At work.** Ana's Python client passes every test against a fake API, a stand-in that answers like the real one. The agent built it with the field names she expected, such as `vendor_id`. Against the sample API, the client is refused on the first invoice, because that API's JSON says `vendorId`. The schema caught it before any invoice was used. She asks for the model to accept both names.

### Where does the key go?

Into an **environment variable**, never into code, a test or a commit. An **API key** is a secret that proves who is calling. An environment variable is a named value the operating system gives a running program, usually loaded from a `.env` file that git ignores. If a key is ever committed, treat it as leaked, even if you delete it in the next commit, because git keeps every commit.

### Try it

**Set up the sample API.** Choose a made-up key, such as a word and a number. Send: "Please copy .env.example to .env, and put my made-up key in it: [your key]. Then start the sample API in the background, and tell me how you will stop it."

**Run.** Send: "Please run the file in 14-6 and show me what it prints. Don't explain it yet."

**Modify.** Send: "Please change page 2 to page 4 in the file. Don't run it yet." There is no page 4, and this API treats that as your mistake, so it answers `400`. From the table, what should a client do then? The file still goes on to read the invoices: what will happen? Then ask the agent to run it, and ask: "What did the body of that answer say?"

**Make.** Send: "Please change the file so it checks the status code before it reads the invoices, and prints the error when the code is not 200. Run it."

**Stop.** Send: "Please stop the sample API now, and show me that nothing is still running."

**What you should see.** `400`, then a `TypeError` or `KeyError`, because the body holds an error, not a page of invoices. After your change, the error is printed plainly.

**The operating rule:** read an API call by its parts, and handle each status code on purpose. Read every page, keep keys in the environment, and validate the body with a schema before you use it, even when your tests pass.

![Figure 14.7. One exchange with the sample API at localhost:4010. The request is GET /invoices with the query string ?status=open&page=2 and an X-API-Key header loaded from the environment. The response is 200 OK, with Content-Type: application/json and a JSON body holding five invoices, page 2 of 3, starting with INV-8306.](img/request-response.png)

*Figure 14.7. One request, one response.*

**Next.** [14.7](async-and-await.md) decides how long to wait for a call, and when to try again.
