Python API Authentication and Errors

Send credentials correctly, distinguish authentication from authorization failures, and handle and retry API errors without repeating unsafe operations.

Lesson 72 of 107Advanced12 min read

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:

Python
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:

SchemeHow it's sentIn requests
API keya header or query parameterheaders={"X-API-Key": key}
Bearer tokenAuthorization: Bearer <token>headers={"Authorization": f"Bearer {token}"}
HTTP Basicusername and password, base64-encodedauth=("user", "password")
OAuth 2.0obtain a short-lived access token first, then send it as a bearer tokena 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.
Python
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:

StageException or signLikely causeWhat to do
Networkrequests.Timeout, requests.ConnectionErrorserver down, DNS, Wi-Firetry reads later; report "service unreachable"
HTTP statusrequests.HTTPError after raise_for_status()4xx: your request; 5xx: the serverfix request, or retry 5xx/429 with limits
Bodyrequests.JSONDecodeError, missing fieldsunexpected format or API changelog and report; don't retry
Python
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, 503 and 504.
  • Don't retry: 400, 401, 403, 404 or 422, which will fail the same way every time.
  • Be careful with POST and 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:

Python
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.

Python
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.

Example 1

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.

Python
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
  1. Line 4The token comes from the environment, so it never appears in the code.
  2. Line 7The bearer token is sent in the Authorization header.
  3. Line 10raise_for_status() turns a 401 or 403 into an exception before decoding.
Example 2

401, 403 and success against a local API

Python
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()
Output
401 invalid or missing token
403 admin role required
OK: {'deleted': '/rides/17'}
Line-by-line explanation
  1. Line 10removeprefix() (Python 3.9+) extracts the token from the header value.
  2. Lines 11 to 16No valid token gives 401; a valid token without the admin role gives 403.
  3. 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

Don't
for attempt in range(5):
    response = session.post(url, json=payment, timeout=10)   # may charge five times
    if response.ok:
        break
Do
response = 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

Don't
print(f"Calling API with {token}")
Do
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.

Multiple choiceeasy

Can a valid token still lack permission for an operation?

Multiple choicemedium

Which review concern is valid for api authentication and errors?

API Authentication and Errors Quiz

2 questions to check what you learned.

  1. Can a valid token still lack permission for an operation?

    • No
    • Yes
    • Python ignores the distinction
  2. 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