Topic 2.1
The HTTP API Under Every SDK
In one line
Calling a model means sending an HTTPS POST with a JSON body (model name, messages, limits) and reading a JSON reply. SDKs wrap this request; knowing the raw shape lets you debug, switch providers and call any model from any language.
Think of it like this
Ordering at a restaurant counter with a printed form. You fill in the form (what you want, how much), hand it over with your membership card, and get back a receipt with the dish and the price. The form is the request, the card is the API key, the receipt is the reply. A model call works the same way, and any waiter (any SDK, any language) that fills in the form correctly gets the same food.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- HTTP
- The standard way programs ask each other for things over the internet: a request goes out, a response comes back.
- Request / response
- What you send (URL, headers, body) and what the server sends back (status, headers, body).
- JSON
- A text format for structured data: objects in curly braces, lists in square brackets. Every model API uses it.
- Endpoint
- One URL of an API that does one job, such as
/v1/chat/completionsfor chat. - Header
- A named extra line sent with a request or response, such as
AuthorizationorContent-Type. - API key
- A secret string that proves who is calling and who should be billed.
- Status code
- The number at the top of a response: 200 means OK, 401 bad key, 429 too many requests, 500 server trouble.
- Fake server
- A function in your own program that pretends to be the remote API so examples run offline.
Step by step
01Step 1: it is just a web request
When you type a web address, your browser sends a GET request and receives HTML. A model call is the same idea with two changes: the method is POST (you send a body) and both bodies are JSON. Nothing in it needs a special library.
02Step 2: build the body as plain data
Make a dictionary (an object in JavaScript) with model, messages and max_tokens, then turn it into JSON text. The first example prints that text so you can see exactly what goes over the wire.
import json
request = {"model": "small-model", "messages": [{"role": "user", "content": "Hi"}], "max_tokens": 50}
print(json.dumps(request))03Step 3: add the headers
The Authorization: Bearer <key> header carries your key. In the example the fake server compares it with the key it expects and answers 401 if it does not match, as a real server does.
04Step 4: read the reply
Check the status first. Only on 200 parse the JSON and walk down: choices[0].message.content is the answer, choices[0].finish_reason says why it stopped, usage has the token counts. Print each so you can match them with the raw JSON line above.
05Step 5: see the failure shape
The last lines of the example send a wrong key. The status is 401 and the body is a different JSON shape with an error object. A client that reads choices without checking the status would crash with a confusing "key not found" instead of telling you the key is wrong.
06Step 6: the same call for real
The blocks after the example show the same call with curl, Python (httpx), JavaScript (fetch) and Java (HttpClient). They differ only in syntax. SDKs (Topic 2.5) wrap exactly this request so you do not write it each time.
Try it yourself
- 1
Predict the token counts
Before running, count the characters of the question (30). The fake counts about 4 characters per token, rounding up. Predict
prompt_tokens, then run and compare. - 2
Change the question
Change the user message so it no longer mentions France. What does the answer become? Where in the fake server is that decided? (A real server uses a model instead of an
if.) - 3
Compare the languages
Run the example in all three languages and compare the
request bodyline. Why is the key order the same everywhere?
Code & diagrams
The fake server checks the key and builds a reply of the real shape. Java uses the course's Json helper; real Java projects use Jackson.
Expected output
POST /v1/chat/completions
request body: {"model":"small-model","messages":[{"role":"user","content":"What is the capital of France?"}],"max_tokens":50}
status: 200
response body: {"id":"chatcmpl-001","object":"chat.completion","model":"small-model","choices":[{"index":0,"message":{"role":"assistant","content":"Paris."},"finish_reason":"stop"}],"usage":{"prompt_tokens":8,"completion_tokens":2,"total_tokens":10}}
answer: Paris.
finish_reason: stop
tokens: 8 in, 2 out
wrong key -> 401: Invalid API keyNot run here: it needs your own GROQ_API_KEY and a network. Other OpenAI-compatible providers change only the URL, key and model name.
# pip install httpx (any HTTP client works: requests, urllib, aiohttp)
import os
import httpx
response = httpx.post(
"https://api.groq.com/openai/v1/chat/completions",
headers={"Authorization": "Bearer " + os.environ["GROQ_API_KEY"]}, # the key comes from the environment
json={
"model": "openai/gpt-oss-20b",
"messages": [{"role": "user", "content": "What is the capital of France?"}],
"max_tokens": 300,
},
timeout=30, # seconds: never call a remote service without a timeout (Topic 2.7)
)
response.raise_for_status() # turns 4xx and 5xx into an exception (Topic 2.6)
reply = response.json()
print(reply["choices"][0]["message"]["content"])
curl is a shell command, so all three tabs show the same text. Run it in a terminal with GROQ_API_KEY set.
# The whole API is one HTTPS request. Replace the URL and model with your provider's.
curl https://api.groq.com/openai/v1/chat/completions \
-H "Authorization: Bearer $GROQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-oss-20b",
"messages": [{"role": "user", "content": "What is the capital of France?"}],
"max_tokens": 300
}'Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Skip the status check
In the Python example, replace the final print line (after the wrong-key call) with code that reads the 401 body as if it were a success: try: then print("wrong key -> " + json.loads(text)["choices"][0]["message"]["content"]), then except KeyError as e: and print("wrong key -> missing field: " + e.args[0]).
Myth vs fact
Myth
You need a special library to call a model.
Fact
A model call is a plain HTTPS request with JSON. Libraries add convenience (types, retries, streaming helpers), not capability.
Myth
Every provider has a completely different API.
Fact
Many providers accept the OpenAI-style chat-completions shape, so switching is often a base URL, key and model name. A few (Anthropic, Google) have their own shapes.
Myth
A successful call (HTTP 200) means the answer is good.
Fact
200 means the request worked. The answer may still be cut off, empty or wrong; check finish_reason and validate the content.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Log the request id (providers return one in a response header such as
x-request-id) with every failed call. Support teams ask for it first. - ▸
Cost is
usagetimes price per token, and input tokens are re-sent on every turn (Topic 2.2). Readusagefrom the reply instead of estimating (Topic 6.1). - ▸
Reuse HTTP connections: creating a new secure (TLS) connection for every call adds tens of milliseconds. Keep one client object per process (
httpx.Client, oneHttpClient, the globalfetchagent do this for you).
Remember this
- 1
Why this exists. A model runs on someone else's powerful computers, not yours. To use it your program must send text over the internet and get text back. The web already has a standard way to do that: HTTP, where a client sends a request (a URL, headers, a body) and a server sends a response (a status number, headers, a body). Model providers chose JSON for the bodies. There is no magic channel: if you can send an HTTP request, you can call a model.
- 2
The request. Four parts matter. The URL (for chat models usually ending in
/chat/completions). The method, alwaysPOST. Headers:Authorization: Bearer <key>proves who you are, andContent-Type: application/jsonsays the body is JSON. The body: at leastmodel(which model) andmessages(the conversation, Topic 2.2); usuallymax_tokens(the most tokens, small pieces of text, it may write, Topic 1.1; some newer OpenAI models call itmax_completion_tokens) and sometimestemperature(how random, Topic 1.6). - 3
The response. A status code first:
200means success, anything else means trouble (Topic 2.6). On success the JSON body holdschoices(a list; the first item has the model'smessageand afinish_reason) andusage(how many tokens went in and came out, which is what you are billed for). Topic 2.3 reads each field. - 4
One shape, many providers. The request shape that OpenAI introduced for "chat completions" became the common dialect: Groq, Together, Mistral, DeepSeek, local servers like Ollama and vLLM and many others accept it, so often only the base URL, the key and the model name change. Anthropic and Google use their own shapes (Topic 2.5). Learning the common one covers most of the market.
- 5
Why build it by hand first. The example below builds the request as a plain dictionary, turns it into compact JSON text, hands it to a fake server function that checks the key and answers, and parses the reply. No network is used, so it is free and always gives the same result, but every byte is the real shape. When an SDK call fails later, you can reason about the bytes it sent.
- 6
Two things never to forget. The key is a secret that costs money if stolen, so it lives in an environment variable on a server (Topic 2.4). And a request can fail or hang, so real code always sets a timeout and checks the status before trusting the body (Topics 2.6, 2.7).
Explain it without notes
What is inside a chat-completions request, and what comes back?
Why can the same client code often talk to several providers?
Why should code check the status code before reading choices?
Practice
Write build_request(question) that returns the request dictionary for a one-message chat to model small-model with max_tokens 50, and print it as compact JSON.
Write describe(status, body_text) that returns "ok: <content>" for status 200 (read choices[0].message.content) and "error <status>: <message>" otherwise (read error.message).
Trade-offs
- ↔
Raw HTTP has no dependencies and never goes out of date, but you write retries, types and streaming yourself; an SDK saves code and adds a dependency.
- ↔
An OpenAI-compatible endpoint maximises portability but may hide provider-specific features (such as prompt caching controls).
Done when you can
Done when you can write the headers and JSON body of a chat-completions request from memory.
Done when you can name the fields to read in the reply:
choices[0].message.content,finish_reasonandusage.Done when you check the status code before parsing a body.
Done when you can run the same call with curl and in one programming language.