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 |
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
.envfiles (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
.envfile exists in integration directoryCheck
.envuses correct variable namesConsider using
required_envin your connection config for automatic validation — see How to Userequired_env
Next Steps¶
How to Use
required_env- Automate env var validation and injectionHow to Handle API Pagination - Work with large datasets
How to Add a REST API Source - Review API source setup
How to Configure Field Mappings - Map API response fields