Most APIs need to know who is calling before they answer. In Python with requests, that usually means sending a secret token in a header:
import os, requests
token = os.environ["CLUB_API_TOKEN"]
response = requests.get("https://api.example.org/members/me",
headers={"Authorization": f"Bearer {token}"}, timeout=10)This lesson covers the common authentication schemes, the difference between authentication (who you are) and authorization (what you may do), how to tell network failures, HTTP errors and bad response data apart, and how to retry only when it's safe. The examples use a cycling club's membership API. It builds on HTTP requests and REST APIs.
Authentication
Authentication proves the caller's identity. The API's documentation says which scheme it uses:
| Scheme | How it's sent | In requests |
|---|---|---|
| API key | a header or query parameter | headers={"X-API-Key": key} |
| Bearer token | Authorization: Bearer <token> | headers={"Authorization": f"Bearer {token}"} |
| HTTP Basic | username and password, base64-encoded | auth=("user", "password") |
| OAuth 2.0 | obtain a short-lived access token first, then send it as a bearer token | a library such as requests-oauthlib or the provider's SDK |
Read credentials from environment variables or a secrets manager, never from source code. Prefer headers to query parameters, since URLs end up in server logs and browser history. Always use https://: over plain HTTP, tokens travel unencrypted.
A requests.Session can hold the header so every call includes it: session.headers["Authorization"] = f"Bearer {token}".
Authorization
Once the API knows who you are, it checks whether you are allowed to do what you asked. The two outcomes have different status codes:
- 401 Unauthorized really means unauthenticated: the credentials are missing, invalid or expired. Fix: check the token is set, correctly formatted and current; refresh it if it's short-lived.
- 403 Forbidden means the credentials are valid but lack permission for this action or resource. Fix: request the right scope or role, or use an account that has it. Retrying won't help.
if response.status_code == 401:
raise PermissionError("Token missing or expired: check CLUB_API_TOKEN")
if response.status_code == 403:
raise PermissionError("This token can't edit rides; ask an admin for the rides:write scope")Tokens are usually issued with limited scopes, such as read-only access to rides. Request the smallest scope your program needs: if the token leaks, the damage is limited to what it could do.
Failure categories
A request can fail at three different stages, and each calls for a different response:
| Stage | Exception or sign | Likely cause | What to do |
|---|---|---|---|
| Network | requests.Timeout, requests.ConnectionError | server down, DNS, Wi-Fi | retry reads later; report "service unreachable" |
| HTTP status | requests.HTTPError after raise_for_status() | 4xx: your request; 5xx: the server | fix request, or retry 5xx/429 with limits |
| Body | requests.JSONDecodeError, missing fields | unexpected format or API change | log and report; don't retry |
try:
response = session.get(url, timeout=10)
response.raise_for_status()
rides = response.json()["rides"]
except requests.Timeout:
print("The club API didn't answer in time")
except requests.HTTPError as error:
print("API returned", error.response.status_code)
except (requests.JSONDecodeError, KeyError):
print("Unexpected response format")One catch-all except Exception: print("API error") hides which case occurred, so nobody knows whether to wait, fix the token or update the code.
Retry policy
Retry only failures that are temporary and operations that are safe to repeat:
- Retry: timeouts and connection errors on reads,
429 Too Many Requests,502,503and504. - Don't retry:
400,401,403,404or422, which will fail the same way every time. - Be careful with
POSTand other writes. If the request reached the server but the response was lost, retrying can repeat the action, such as charging a membership fee twice. Retry writes only when the API supports an idempotency key, a unique id you send so the server ignores duplicates.
Wait between attempts with exponential back-off (1, 2, 4 seconds...), cap the number of attempts, and respect a Retry-After header when the server sends one. requests can do this for you with urllib3's Retry class:
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(total=3, backoff_factor=1, status_forcelist=[429, 502, 503, 504],
allowed_methods=["GET"])
session.mount("https://", HTTPAdapter(max_retries=retry))Independent practice and a worked check
Task: build a client with external credentials, explicit timeout handling and bounded retries for safe reads.
import os, time
import requests
def get_rides(url, attempts=3):
token = os.environ["CLUB_API_TOKEN"]
headers = {"Authorization": f"Bearer {token}"}
for attempt in range(1, attempts + 1):
try:
response = requests.get(url, headers=headers, timeout=10)
except (requests.Timeout, requests.ConnectionError):
if attempt == attempts:
raise
else:
if response.status_code in (429, 502, 503, 504) and attempt < attempts:
pass # temporary: retry below
else:
response.raise_for_status() # 401/403/404 fail immediately
return response.json()
time.sleep(2 ** (attempt - 1)) # 1 s, then 2 s
response.raise_for_status()Only GET requests are retried, the number of attempts is capped, and 401 or 403 raise straight away because waiting won't fix them. For the worked check: can a valid token still lack permission for an operation? Yes. That's a 403, an authorization failure.
Reference and next practice
The requests documentation covers authentication, and urllib3 documents Retry. Continue with a simple web server and revisit environment configuration. Practise with the Python exercises and Python quiz.
API Authentication and Errors Summary
- Authentication: Tokens or credentials identify a caller according to the API contract.
- Authorization: A service checks whether the authenticated caller may perform the requested action.
- Failure categories: Network failures, error statuses and invalid response data need separate diagnoses.
- Retry policy: Retry only selected temporary failures with limits and respect service rate guidance.
API Authentication and Errors Examples
Each example shows the code, the output it produces and what each part does.
API Authentication and Errors in a small program
This needs a real API and CLUB_API_TOKEN set; the URL is illustrative. The next example runs offline against a local server.
import os
import requests
token = os.environ["CLUB_API_TOKEN"]
response = requests.get(
"https://example.org/members/me",
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
response.raise_for_status()
print(response.json())Line-by-line explanation
- Line 4The token comes from the environment, so it never appears in the code.
- Line 7The bearer token is sent in the
Authorizationheader. - Line 10
raise_for_status()turns a 401 or 403 into an exception before decoding.
401, 403 and success against a local API
import json, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests
TOKENS = {"member-token": "member", "admin-token": "admin"}
class ClubAPI(BaseHTTPRequestHandler):
def do_DELETE(self):
auth = self.headers.get("Authorization", "")
role = TOKENS.get(auth.removeprefix("Bearer "))
if role is None:
status, body = 401, {"error": "invalid or missing token"}
elif role != "admin":
status, body = 403, {"error": "admin role required"}
else:
status, body = 200, {"deleted": self.path}
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(body).encode())
def log_message(self, *args):
pass
server = HTTPServer(("127.0.0.1", 0), ClubAPI)
threading.Thread(target=server.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{server.server_address[1]}/rides/17"
for token in [None, "member-token", "admin-token"]:
headers = {"Authorization": f"Bearer {token}"} if token else {}
response = requests.delete(url, headers=headers, timeout=5)
try:
response.raise_for_status()
print("OK:", response.json())
except requests.HTTPError:
print(response.status_code, response.json()["error"])
server.shutdown()401 invalid or missing token
403 admin role required
OK: {'deleted': '/rides/17'}Line-by-line explanation
- Line 10
removeprefix()(Python 3.9+) extracts the token from the header value. - Lines 11 to 16No valid token gives 401; a valid token without the admin role gives 403.
- Lines 31 to 35The client reads the API's JSON error message to explain each failure.
Common API Authentication and Errors Mistakes
Retrying a 403 or a non-idempotent POST
for attempt in range(5):
response = session.post(url, json=payment, timeout=10) # may charge five times
if response.ok:
breakresponse = session.post(url, json=payment, timeout=10,
headers={"Idempotency-Key": payment_id}) # if the API supports it
if response.status_code == 403:
raise PermissionError("missing payments:write scope")Retry only temporary failures, and only for operations the server can safely receive twice.
Printing or logging the token
print(f"Calling API with {token}")print(f"Calling API with token ending ...{token[-4:]}")API Authentication and Errors in Real Projects
Authentication in real integrations
Integrations with payment providers, CRMs, cloud services and social platforms all follow these patterns. Services such as Stripe and Adyen support idempotency keys precisely so clients can retry payments safely. OAuth 2.0 is standard for acting on behalf of users, with short-lived access tokens refreshed automatically by SDKs. Server-to-server integrations often use API keys stored in a secrets manager and rotated regularly.
Production clients log the status code, the API's request id and the operation, never the credentials, and alert on rising 401s, which often mean a token has expired or been revoked.
API Authentication and Errors FAQ
How do I send a bearer token with Python requests?
Add an Authorization header: requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=10). With a session, set session.headers["Authorization"] once and every request includes it.
What is the difference between 401 and 403?
401 means the API couldn't authenticate you: the token is missing, invalid or expired. 403 means it knows who you are but you're not allowed to perform that action. Refresh or fix credentials for 401; request permissions for 403.
Should I put API keys in the URL?
Avoid it when the API allows headers. URLs are written to server logs, proxies and browser history, so a key in the query string is easier to leak. Some APIs require query-string keys; then make sure you use HTTPS and keep logs private.
How do I retry failed requests in Python?
Mount an HTTPAdapter with a urllib3 Retry configuration on a session, limited to safe methods and temporary statuses (429, 502, 503, 504), with a back-off factor. For custom logic, write a loop with a capped number of attempts and increasing sleeps.
What is an idempotency key?
A unique value you send with a write request so the server can recognise duplicates. If a retry arrives with the same key, the server returns the original result instead of performing the action again. Payment APIs commonly support it.
API Authentication and Errors Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Can a valid token still lack permission for an operation?
Which review concern is valid for api authentication and errors?
API Authentication and Errors Quiz
2 questions to check what you learned.
Can a valid token still lack permission for an operation?
- No
- Yes
- Python ignores the distinction
Which statement correctly describes failure categories?
- Network failures, error statuses and invalid response data need separate diagnoses.
- Only the example output matters, not the rule
- Every operation has the same behavior