Every app you've ever used is two computers passing notes. HTTP is the note format — and it's so simple you can read it raw. This page: the request/response model, what the status codes are actually telling you (401 vs 403, settled forever), REST without the theology, and the truth about the CORS error.
All of HTTP is one exchange: the client sends a request (a structured note), the server sends back a response (another note), and then — this is the part people miss — the server forgets you existed. HTTP is stateless. Every "logged-in experience" you've ever had is built by re-showing an ID card (a cookie or token) with every single note.
## the request — literally this readable:
GET /users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbG...
## the response:
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 42, "name": "Ada"}
A URL is an address with four working parts: the protocol, the building (host), the room (path), and the sticky notes on the door (query parameters). Learn to read them and API docs stop being scary.
https://api.example.com/users/42/orders?status=paid&limit=10
└─┬──┘ └──────┬──────┘└──────┬──────┘ └────────┬─────────┘
scheme host path query params
(how) (which server) (which thing) (options: key=value, &-separated)
?api_key=sk_live_... is a key you should already consider leaked. Secrets ride in headers (section 07), which don't get logged by default.
The method tells the server what kind of note this is. Five cover everything: GET reads, POST creates, PUT replaces, PATCH edits a part, DELETE deletes. The concept behind them worth actually understanding: idempotency — can I safely send this twice?
GET /users/42 # read — never changes anything
POST /users # create a new one ⚠ twice = two users
PUT /users/42 # replace entirely — twice = same result
PATCH /users/42 # change some fields
DELETE /users/42 # delete — twice = still deleted
Idempotency-Key header, so a retried request creates one charge, not two. When a network times out mid-POST, "did it go through?" is the whole reason this word exists.
The first digit is the whole story: 2xx worked, 3xx moved, 4xx you messed up, 5xx they messed up. That one sentence sorts every API error you'll ever see into "fix my request" vs "wait / report it". And the eternal confusion pair, settled:
200 OK # here you go
201 Created # made it (answer to a good POST)
301 Moved # new address, go there
400 Bad Request # your note is malformed (bad JSON, missing field)
401 Unauthorized # WHO ARE YOU? — no/invalid credentials
403 Forbidden # I know who you are. No. — valid login, no rights
404 Not Found # no such thing at this path
429 Too Many Requests # slow down (rate limit — wait and retry)
500 Internal Error # server crashed. not your fault
503 Unavailable # server overloaded/down. also not your fault
Headers are metadata on the envelope — what format, what language, who's asking, what credentials. The body is the letter inside — for APIs, almost always JSON. You'll set two headers constantly and read a third:
Content-Type: application/json # "the body I'm SENDING is JSON"
Authorization: Bearer <token> # "here's my ID card"
Accept: application/json # "please REPLY in JSON"
# the body (POST /users):
{ "name": "Ada", "country": "UK" }
400 Bad Request or a weird null-everything — because you forgot Content-Type: application/json, and the server parsed your JSON as a form submission. The body was fine. The label on the envelope was wrong. This one header explains a decade of Stack Overflow questions.
curl sends HTTP requests from the command line. It's how you test an API without your app in the way — the single best debugging move: if curl works and your code doesn't, the bug is in your code; if curl fails too, it's the API. Divide and conquer.
# read
curl https://api.example.com/users/42
# create (-X method, -H header, -d body)
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name": "Ada"}'
# see the STATUS CODE and headers too (the debugging view)
curl -i https://api.example.com/users/42
Two patterns cover most APIs. An API key is a long-lived password for a program. A bearer token is usually short-lived, obtained by logging in, and presented as Authorization: Bearer … — "bearer" literally means whoever carries this, is this. Which is the security model and the security warning in one word.
# both ride in the Authorization header:
Authorization: Bearer eyJhbGciOi... # token (often a JWT)
x-api-key: sk_live_4242... # some APIs use a custom header
# keys live in environment variables, never in code:
curl -H "Authorization: Bearer $API_TOKEN" ...
REST is a naming convention, not a technology: URLs name things (nouns), methods say what to do to them (verbs). That's 90% of it. An API is "RESTful" when you can guess the next endpoint without reading the docs — predictability is the entire point.
# nouns in the URL, verbs in the method:
GET /articles # list articles
POST /articles # create one
GET /articles/7 # read one
PATCH /articles/7 # edit it
DELETE /articles/7 # remove it
GET /articles/7/comments # nested: its comments
✗ POST /getArticleById ✗ GET /deleteUser?id=7 # verbs in URLs = smell
One day your frontend calls an API and the console screams. Here's the twist everyone misses: this error is not the server rejecting you — it's your own browser, enforcing a safety rule: JavaScript from site A may not read responses from site B unless B explicitly allows it (via an Access-Control-Allow-Origin header). The proof: the same request works fine in curl.
Access to fetch at 'https://api.other.com/data' from origin
'http://localhost:3000' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present...
# decode: api.other.com didn't say localhost:3000 may read it.
# the browser fetched the data — then withheld it from your JS.
The mental tables that answer 90% of API questions.
# methods
GET read · POST create (⚠ not retry-safe) · PUT replace
PATCH edit · DELETE remove
# status: first digit = who messed up
2xx ✓ · 3xx moved · 4xx you · 5xx them
401 who are you? → fix the token
403 you specifically? no. → fix the permissions
429 slow down → wait and retry
# the POST that "mysteriously" fails
→ did you send Content-Type: application/json ?
# debugging order
curl -i first. code second.
curl works + code fails → your bug
curl fails too → their bug (or your token)
# CORS error in console
→ browser rule, server must allow it. curl proves the API works.
# secrets
headers ✓ · env vars ✓ · URLs ✗ · git ✗ · frontend ✗
That's the working core. Everything fancier — webhooks, GraphQL, gRPC — is a variation on the same two notes. When you're ready to call APIs from code, Python plus this page is a complete toolkit.