APIs for AIFastAPI 0.141 · httpx2 2.13 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
19 small wins to finish your pathNext lesson

API keys: headers and environment variables

A real model API refuses a request that does not say who is paying. The key travels in a header, and it belongs in an environment variable, never in your code.

Give model_api.py a key check. The imports gain Annotated, Header and HTTPException, and two pieces change:

Examplemodel_api.py, new
API_KEY = "sk-local-123"


def check_key(authorization: str | None):
    if authorization != f"Bearer {API_KEY}":
        raise HTTPException(status_code=401, detail="missing or wrong API key")
Examplemodel_api.py, the endpoint now
@app.post("/v1/chat/completions")
def chat(request: ChatRequest, authorization: Annotated[str | None, Header()] = None):
    check_key(authorization)
    question = request.messages[-1].content
    answer = decide(question)
    return {
        "model": request.model,
        "choices": [{"index": 0, "message": {"role": "assistant", "content": answer}}],
        "usage": {"prompt_tokens": len(question.split()), "completion_tokens": len(answer.split())},
    }

Annotated[str | None, Header()] tells FastAPI to read authorization from the request's Authorization header instead of the query string. Bearer followed by the key is the usual way to send an API key, and it is what OpenAI and most other providers expect.

Example
from fastapi.testclient import TestClient
from model_api import app

body = {"model": "local", "messages": [{"role": "user", "content": "I was charged twice"}]}

no_key = TestClient(app)
print(no_key.post("/v1/chat/completions", json=body).status_code)

with_key = TestClient(app, headers={"Authorization": "Bearer sk-local-123"})
print(with_key.post("/v1/chat/completions", json=body).status_code)

headers= on the client adds that header to every request it sends, so the key is set once.

Keeping the key out of the code

A key written in a .py file ends up in version control, and from there in every copy of the project. Read it from an environment variable, a named value the terminal hands to every program it starts:

Examplecall.py
import os

from fastapi.testclient import TestClient
from model_api import app

key = os.environ["MODEL_API_KEY"]
model = TestClient(app, headers={"Authorization": f"Bearer {key}"})

body = {"model": "local", "messages": [{"role": "user", "content": "I was charged twice"}]}
response = model.post("/v1/chat/completions", json=body)
print(response.status_code, response.json()["choices"][0]["message"]["content"])
Example
export MODEL_API_KEY=sk-local-123
python call.py

export sets the variable for this terminal. os.environ is a dictionary of every environment variable. Now forget to set it:

Example
python call.py

A KeyError on the first line that needs the key is the right failure: loud, immediate, and naming the variable. os.environ.get with a default would hide the problem until the model API answered 401.

On Windows
In PowerShell set it with $env:MODEL_API_KEY = "sk-local-123".
Try it yourself
  • Send Bearer sk-wrong and print the response body.
  • Put MODEL_API_KEY=sk-local-123 in a file called .env, install python-dotenv, and call load_dotenv() at the top of call.py. Add .env to .gitignore.
  • Print key[:6] + "..." instead of the key when you need to check which one is loaded.

Every expert started right here.