How to Authenticate with API Keys

TL;DR

Configure API key authentication for your REST API connection to securely access protected endpoints.

Note: For OAuth 2.0 client credentials authentication, see How to Authenticate with Client Credentials. This guide covers simpler API key authentication.

Prerequisites

  • A REST API source already set up (see How to Add a REST API Source)

  • API key from your service provider

  • Understanding of how your API expects the key to be passed (header, query parameter, etc.)

When to Use API Keys vs Client Credentials

Feature

API Keys

Client Credentials (OAuth 2.0)

Complexity

Simple

More complex setup

Token Expiration

Static key (manual rotation)

Auto-refreshes tokens

Security

Suitable for simpler APIs

Better for production

Framework Support

Manual header/param setup

Built-in via oaa.modules.oauth_creds

Best For

Simple REST APIs

Enterprise APIs

For more complex authentication needs with automatic token management, see How to Authenticate with Client Credentials.

Steps

Step 1: Store Your API Key Securely

Create or update .env file in your integration directory:

# .env (never commit this file!)
API_KEY=your_api_key_here
MY_API_TOKEN=sk-abc123def456ghi789

Add .env to .gitignore:

echo ".env" >> .gitignore

Step 2: Update Your Connection Module

Modify modules/my_api_client.py to read the API key from environment:

import os
import requests

class MyAPIClient:
    def __init__(self, base_url=None, **kwargs):
        """Initialize with API key from environment or config"""
        self.base_url = base_url
        self.api_key = kwargs.get('api_key') or os.getenv('API_KEY')
        
        if not self.api_key:
            raise ValueError("API_KEY not found in environment or config")
        
        self.session = requests.Session()
        
        # Common header pattern: Bearer token
        self.session.headers.update({
            'Authorization': f'Bearer {self.api_key}'
        })
    
    def fetch(self, resource, **params):
        url = f"{self.base_url}{resource}"
        try:
            response = self.session.get(url, params=params, timeout=30)
            response.raise_for_status()
            
            data = response.json()
            items = data.get('items', data.get('results', data.get('data', [])))
            
            yield from items
            
        except requests.RequestException as e:
            raise RuntimeError(f"API request failed: {e}")

Step 3: Configure in config.yaml

Update your connection configuration to pass the API key:

connections:
  my_api:
    connection_type: custom
    custom_module: my_api_client
    params:
      base_url: https://api.example.com
      api_key: ${API_KEY}  # Reads from .env or environment

The ${API_KEY} syntax tells OAA-runner to replace this with the environment variable value.

Note on .env loading: If you declare required_env in your connection config (see How to Use required_env), oaa-runner automatically loads .env files and injects them into connection.params — you don’t need to manually call load_dotenv() in your module.

Step 4: Test Authentication

Validate your configuration:

uv tool run --from oaa-runner oaa app validate --config=config.yaml

Run the integration to test the connection:

uv tool run --from oaa-runner oaa app run --config=config.yaml --save-json

Check for authentication errors in the logs.

API Key Patterns

Different APIs use different authentication patterns. Here are common ones:

Bearer Token (Header)

headers = {
    'Authorization': f'Bearer {api_key}'
}

API Key in Header

headers = {
    'X-API-Key': api_key,
    # or sometimes:
    'Api-Key': api_key
}

Query Parameter

params = {
    'api_key': api_key,
    'resource': 'users'
}

Basic Auth

from requests.auth import HTTPBasicAuth

auth = HTTPBasicAuth(username, api_key)
response = session.get(url, auth=auth)

Examples

Real Integration: ThousandEyes

class ThousandEyesAPI:
    def __init__(self, thousandeyes_base_uri=None, **kwargs):
        self.base_uri = thousandeyes_base_uri
        self.api_token = kwargs.get('api_token') or os.getenv('THOUSANDEYES_API_TOKEN')
        
        self.session = requests.Session()
        self.session.auth = (self.api_token, '')  # ThousandEyes uses basic auth
    
    def fetch(self, resource, **params):
        url = f"{self.base_uri}/{resource}"
        response = self.session.get(url, params=params)
        response.raise_for_status()
        yield from response.json().get('results', [])

From .env:

THOUSANDEYES_API_TOKEN=your_token_here

Real Integration: Abnormal Security

class AbnormalAPI:
    def __init__(self, abnormal_api_base_url=None, **kwargs):
        self.base_url = abnormal_api_base_url
        self.api_key = kwargs.get('api_key') or os.getenv('ABNORMAL_API_KEY')
        
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'Bearer {self.api_key}',
            'Content-Type': 'application/json'
        })
    
    def fetch(self, resource, **params):
        url = f"{self.base_url}{resource}"
        response = self.session.get(url, params=params)
        response.raise_for_status()
        yield from response.json().get('users', [])

Security Best Practices

✅ DO:

  • Store API keys in .env files (add to .gitignore)

  • Use environment variables in production

  • Rotate API keys regularly

  • Use different keys for dev/staging/production

  • Limit key permissions to minimum needed

❌ DON’T:

  • Commit API keys to git

  • Hardcode keys in source code

  • Share keys in chat or email

  • Use the same key for multiple environments

  • Log API keys (they may appear in debug logs)

Troubleshooting

Error: 401 Unauthorized

  • Verify API key is correct

  • Check that authentication header matches API requirements

  • Ensure key hasn’t expired

Error: 403 Forbidden

  • API key may lack required permissions

  • Check API documentation for required scopes

  • Verify key is active

Error: API_KEY not found in environment

  • Verify .env file exists in integration directory

  • Check .env uses correct variable names

  • Consider using required_env in your connection config for automatic validation — see How to Use required_env

Next Steps