(+351) 21 24 10006  ·  info@bconcepts.pt
Carnaxide, Lisbon

How to Authenticate a REST API with OAuth 2.0 in Python

João Barros 05 de July de 2026 4 min read

The most robust data APIs are not open to the public: they require authentication. In server-to-server scenarios, the most common standard is OAuth 2.0 with the client credentials flow, and mastering it is essential for building reliable data pipelines. This guide shows, step by step, how to obtain an access token and call a protected endpoint in Python.

Prerequisites

  • Python 3.8 or later installed.
  • The requests library (install it with pip install requests).
  • Your API credentials: client_id and client_secret, provided by the vendor's portal.
  • The token endpoint URL (where tokens are requested) and the URL of the protected resource you want to consume.

Step 1: Understand the client credentials flow

OAuth 2.0 defines several flows. The client credentials flow is the right one when it is your application — not a user — that accesses the data, as happens in an ETL script or a scheduled service. The idea is simple: the application presents its client_id and client_secret to a token endpoint, receives a temporary access token in return, and uses that token to authenticate every request to the API. There are no login screens and no human intervention, which makes it ideal for automation.

Step 2: Get the access token

The first request is a POST to the token endpoint, specifying grant_type=client_credentials. The response is a JSON object with the token and its lifetime in seconds (expires_in). Note that the credentials go in the request body, over HTTPS, and never in the URL.

import requests

TOKEN_URL = "https://auth.example.com/oauth/token"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"

def get_token():
    response = requests.post(TOKEN_URL, data={
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "scope": "data.read",
    }, timeout=10)
    response.raise_for_status()
    return response.json()["access_token"]

token = get_token()
print("Access token obtained")

The raise_for_status() method ensures that a credentials error (HTTP 401) stops the script immediately, instead of continuing with an invalid token.

Step 3: Call the protected endpoint

With the token in hand, every request to the API carries the Authorization: Bearer <token> header. That is how the server knows you are authorized.

API_URL = "https://api.example.com/v1/customers"

def get_data(token):
    headers = {"Authorization": f"Bearer {token}"}
    response = requests.get(API_URL, headers=headers, timeout=10)
    response.raise_for_status()
    return response.json()

records = get_data(token)
print(f"Received {len(records)} records")

Step 4: Reuse the token and handle expiry

Requesting a new token on every call is slow and often subject to limits. The best approach is to store the token and its expiry time, refreshing it only when little time is left. A simple class solves the problem:

import time

class APIClient:
    def __init__(self):
        self._token = None
        self._expires_at = 0

    def valid_token(self):
        # Refresh 60 seconds before expiry, to be safe
        if not self._token or time.time() > self._expires_at - 60:
            response = requests.post(TOKEN_URL, data={
                "grant_type": "client_credentials",
                "client_id": CLIENT_ID,
                "client_secret": CLIENT_SECRET,
            }, timeout=10)
            response.raise_for_status()
            payload = response.json()
            self._token = payload["access_token"]
            self._expires_at = time.time() + payload["expires_in"]
        return self._token

client = APIClient()
headers = {"Authorization": f"Bearer {client.valid_token()}"}

This way, the same token is reused while it is valid and refreshed automatically when needed.

Verify the result

To confirm everything works, run the script and check two signals: the token request returns HTTP 200, and the API call returns data instead of a 401 error. If you get a 401, review the client_id, the client_secret and the scope. A 403 error means the token is valid but lacks permission for that resource — in that case, the problem lies in the scopes granted to the application.

Conclusion

With these steps you have a solid foundation for authenticating any data API that uses OAuth 2.0 client credentials, from obtaining the token to refreshing it automatically. The natural next step is to combine this with pagination and writing to a database, turning these calls into a complete pipeline. Which API will you integrate first?