OwlTheEngineer

What is an API, really?

Not the definition. The four things you actually send, what each one is for, and how to find the one that's wrong.

Owl Backend 5 min read

Every definition of “API” you’ve read is technically correct and completely useless. “Application Programming Interface” tells you nothing you can act on at 2am with a red build and a 403 you don’t understand.

Here’s the version that helps.

An API call is four decisions

When you call an HTTP API, you are filling in four blanks. Every single time.

GET https://api.example.com/products/42
Authorization: Bearer sk_live_7f3a...
PartWhat it answers
GETWhat do you want done to it?
https://api.example.comWhose computer?
/products/42Which thing?
AuthorizationWho’s asking?

That’s it. Query strings, bodies, content types, pagination cursors — all of it is detail hung off one of those four blanks. Learn to see a request as four answers and most “weird API behaviour” resolves into “blank three was wrong”.

The verb is a promise

The method isn’t a label. It’s a promise about what happens if the request runs more than once.

  • GET promises it changes nothing. That promise is why a browser can prefetch it, why a CDN can cache it, and why retrying after a timeout is free.
  • PUT and DELETE promise the same end state every time. Sending PUT /users/42 twice leaves you with the same user, not two.
  • POST promises nothing. Which is exactly why it’s the one that hurts.

Why your payment provider wants an idempotency key

The network cannot tell these two failures apart:

  1. The request never arrived.
  2. The request arrived, ran, and the response was lost coming back.

From the client, both look like a timeout. If you retry case 1, you’re correct. If you retry case 2, you charged the customer twice.

Since POST makes no promise, something else has to. That something is a key you generate and send:

POST /v1/charges
Idempotency-Key: 7f3a91c2-4e11-4b0a-9a6d-2c8f0b1e5d33

{ "amount": 2999, "currency": "usd" }

The server stores the result against that key. Second request, same key: it replays the stored response instead of charging again. The retry became safe because you made it safe — not because HTTP did.

If a call can cost money or send an email, decide before you write the retry how the second attempt will be recognised.

Which thing: path, query, or body?

Three places to put data, and people put it in the wrong one constantly.

Put it inWhen it isExample
The pathIdentity — which resource/products/42
The query stringA filter, sort or page over a set/products?status=live&page=2
The bodyThe new state you’re sending{"price": 2999}

The test: could you bookmark it? If the answer is naturally “yes, and it means the same thing tomorrow”, it belongs in the URL. A page of search results, yes. A password, absolutely not — URLs end up in browser history, proxy logs, and error trackers.

The one exception people trip on

GET with a body is legal and almost universally ignored — proxies, caches and some HTTP clients will silently drop it. If your query is genuinely too big for a URL, use POST /search and accept that you’ve given up caching. That’s a real trade, not a workaround.

Who’s asking

The fourth blank is the one that fails most, and it fails in a way that looks like something else. Three headers cause most of it:

Authorization: Bearer eyJhbGciOi...   ← who you are
Content-Type:  application/json       ← what you're sending
Accept:        application/json       ← what you want back

Miss Content-Type and a server that would happily accept your JSON returns 400 on a body it never even parsed. Miss Accept and you get HTML — an error page you then try to JSON.parse, producing a stack trace that mentions neither auth nor content type.

The useful reflex: when a call fails in a confusing way, print the request you actually sent, not the one you meant to send.

curl -v https://api.example.com/products/42 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"

-v prints the real headers. Nine times out of ten the bug is visible right there: an empty $TOKEN, a stale key, a trailing newline pasted in from a terminal.

What comes back

A response is a status code and a body. The code is for machines; the body is for you.

{
  "id": 42,
  "name": "Wireless Mouse",
  "price": 2999,
  "currency": "usd",
  "stock": 17
}

The mistake juniors make is reading the body and ignoring the code. 200 with an error message inside is a badly designed API — annoying, but survivable. 500 with a perfectly good-looking body is a trap: you’ll parse it, store it, and find out three days later when the numbers don’t reconcile.

The codes worth knowing by heart

CodeIt meansRetry?
400Your request is malformedNo — fix it
401Not authenticated. No credential, or a bad oneNo
403Authenticated, not allowedNo
404No such thing — or you can’t see itNo
409Conflict; something changed under youRe-read, then retry
422Well-formed, but semantically wrongNo
429Too many requestsYes — after Retry-After
500They brokeYes, with backoff
503They’re down or overloadedYes, with backoff

The 401 vs 403 split is the one interviewers ask about, and it’s genuinely useful: 401 means try again with a credential, 403 means stop, a different credential won’t help.

Notice which rows say “Yes”. A retry policy that retries 400 is a policy that hammers a server with a request that can never succeed.

Errors are part of the contract

An API that returns a bare 500 and the string "error" has documented nothing. A useful error is machine-readable and human-readable at once:

{
  "error": {
    "code": "insufficient_stock",
    "message": "Only 3 units of SKU-9931 remain.",
    "field": "items[0].quantity",
    "request_id": "req_01HQ8Z3K"
  }
}
  • code is stable, so your client can branch on it. Never branch on message.
  • message is for a human reading a log or a toast.
  • field lets a form highlight the right input.
  • request_id is what you paste into a support ticket.

If you’re designing the API: pick your error shape on day one. Changing it later breaks every client that ever handled an error correctly.

Versioning is a promise you can’t take back

The moment someone else calls your API, its shape is a commitment. Two rules keep that survivable:

  1. Adding is safe. Removing and renaming are not. A new optional field breaks nobody. Renaming price to amount breaks everyone, silently, as undefined.
  2. Version when you break, not when you change. /v2 for a genuinely different contract; a new field doesn’t need one.

The corollary for clients: ignore fields you don’t know about. A client that throws on an unexpected key turns the provider’s safest kind of change into your outage.

Debugging a failing call, in order

Work the four blanks. It takes two minutes and beats guessing every time.

  1. Whose computer? Is the host right — staging vs production, the classic. Does DNS resolve? Is it the URL your config actually loaded, or the default?
  2. Which thing? Print the final URL after every bit of interpolation. Half of all 404s are /products/undefined.
  3. Who’s asking? Print the credential’s length, never the credential. Empty string and expired token look identical in code and different in curl -v.
  4. What did you ask for? Check Content-Type and Accept against what the docs say.

Then read the status code before the body. Always.

The one thing to remember

An API is a contract about four blanks and what comes back. When a call fails, the fault is almost always in one blank you didn’t think about — and it’s usually the fourth.