Authentication¶
The buyer agent has two distinct authentication concerns:
- Inbound authentication --- protecting the buyer's own API from unauthorized callers.
- Outbound authentication --- attaching the correct credentials when the buyer calls seller endpoints.
This page covers both, including the ApiKeyStore for managing per-seller credentials and the AuthMiddleware that injects keys into outgoing requests.
Seller-side reference
For full details on how the seller validates keys, issues access tiers, and manages trust levels, see the Seller Authentication docs.
Inbound Authentication¶
X-Api-Key Header¶
Protected endpoints on the buyer agent require the X-Api-Key header:
The middleware compares the provided key against the api_key setting. If the key is missing or does not match, the server returns 401:
Development Mode¶
When api_key is empty (the default), authentication is disabled entirely. All requests are allowed without a key. This is the intended mode for local development.
To enable auth, set the API_KEY environment variable or add it to your .env file:
Public Paths¶
The following paths always skip authentication, even when an API key is configured:
| Path | Purpose |
|---|---|
/health |
Health check |
/docs |
Swagger UI |
/openapi.json |
OpenAPI schema |
/redoc |
ReDoc documentation |
Configuration¶
The API key is loaded through pydantic-settings in ad_buyer.config.settings.Settings:
class Settings(BaseSettings):
# Inbound API key for authenticating requests to this service.
# When empty/not set, authentication is disabled (development mode).
api_key: str = ""
Set it via any method that pydantic-settings supports: environment variable, .env file, or constructor argument.
Outbound Authentication¶
When the buyer calls seller endpoints (quotes, deals, media kits, etc.), it needs to present valid credentials. The seller uses these credentials to determine the buyer's access tier and unlock tiered pricing, negotiation capabilities, and richer data in responses.
How Sellers Authenticate Buyers¶
Sellers accept credentials in two formats on every endpoint:
Unauthenticated requests receive public-tier access only --- price ranges instead of exact prices, no negotiation, and limited data.
Seller Access Tiers¶
The tier the buyer receives depends on the identity fields associated with its API key:
| Tier | Identity Required | Pricing Visibility | Negotiation |
|---|---|---|---|
public |
None (anonymous) | Price ranges only | No |
seat |
DSP seat ID | Exact prices, no discounts | Limited |
agency |
Agency ID | Tier discounts applied | Standard |
advertiser |
Full advertiser identity | Full discounts + volume pricing | Premium |
Maximize your access
When creating an API key on the seller, include seat_id, agency_id, and advertiser_id to receive the highest tier. Keys with only a seat_id will be limited to seat-tier pricing.
ApiKeyStore¶
The ApiKeyStore provides file-backed credential storage for seller API keys. It stores one key per seller URL in a JSON file at ~/.ad_buyer/seller_keys.json. Values are base64-encoded on disk to prevent accidental exposure in casual file reads.
Not encryption
Base64 encoding is an obfuscation layer, not encryption. For production deployments, back the store with a secrets manager or encrypted file system.
Initialization¶
from ad_buyer.auth.key_store import ApiKeyStore
# Default location: ~/.ad_buyer/seller_keys.json
store = ApiKeyStore()
# Custom location
from pathlib import Path
store = ApiKeyStore(store_path=Path("/etc/ad_buyer/keys.json"))
The store loads existing keys from disk on initialization. If the file does not exist, the store starts empty.
Store a Key¶
Add or replace the API key for a seller URL:
The key is persisted to disk immediately. Trailing slashes on the URL are stripped for consistent lookup.
Retrieve a Key¶
key = store.get_key("http://seller.example.com:8001")
if key:
print(f"Key: {key}") # "sk-abc123secret"
else:
print("No key stored for this seller")
Returns None if no key is stored for the given URL.
Remove a Key¶
removed = store.remove_key("http://seller.example.com:8001")
print(removed) # True if the key existed, False otherwise
The file is updated on disk immediately after removal.
Rotate a Key¶
Replace an existing key with a new one. This is functionally identical to add_key but communicates intent:
List All Sellers¶
Return all seller URLs that have a stored key:
sellers = store.list_sellers()
for url in sellers:
print(url)
# http://seller-a.example.com:8001
# http://seller-b.example.com:8001
Full ApiKeyStore API¶
| Method | Signature | Returns | Description |
|---|---|---|---|
add_key |
(seller_url: str, api_key: str) |
None |
Store or replace a key |
get_key |
(seller_url: str) |
str \| None |
Retrieve a key |
remove_key |
(seller_url: str) |
bool |
Remove a key; True if it existed |
rotate_key |
(seller_url: str, new_key: str) |
None |
Replace with a new key |
list_sellers |
() |
list[str] |
All seller URLs with stored keys |
AuthMiddleware¶
The AuthMiddleware sits between the buyer's HTTP clients and the network. It automatically attaches stored API keys to outgoing requests and inspects responses for 401 status codes that indicate expired or revoked credentials.
Initialization¶
from ad_buyer.auth.key_store import ApiKeyStore
from ad_buyer.auth.middleware import AuthMiddleware
store = ApiKeyStore()
store.add_key("http://seller.example.com:8001", "sk-abc123secret")
# Send keys as X-Api-Key header (default)
middleware = AuthMiddleware(key_store=store, header_type="api_key")
# Or send keys as Bearer tokens
middleware = AuthMiddleware(key_store=store, header_type="bearer")
| Parameter | Type | Default | Description |
|---|---|---|---|
key_store |
ApiKeyStore |
required | The key store holding per-seller credentials |
header_type |
"api_key" \| "bearer" |
"api_key" |
How to send the key in the HTTP header |
Adding Auth to Requests¶
The add_auth method takes an httpx.Request and returns a new request with the appropriate auth header attached. If no key is stored for the request's seller URL, the request is returned unchanged.
import httpx
request = httpx.Request("GET", "http://seller.example.com:8001/api/v1/products")
authenticated_request = middleware.add_auth(request)
# The returned request now includes:
# X-Api-Key: sk-abc123secret (if header_type="api_key")
# Authorization: Bearer sk-abc123 (if header_type="bearer")
The middleware extracts the base URL (scheme://host:port) from the request URL and looks up the key in the store. This means all paths on the same seller host share a single key.
Handling 401 Responses¶
The handle_response method inspects HTTP responses and returns an AuthResponse indicating whether re-authentication is needed:
from ad_buyer.auth.middleware import AuthResponse
response = await http_client.send(authenticated_request)
auth_result: AuthResponse = middleware.handle_response(response)
if auth_result.needs_reauth:
print(f"Key expired for {auth_result.seller_url}")
# Acquire a new key from the seller and update the store
new_key = await acquire_new_key(auth_result.seller_url)
store.rotate_key(auth_result.seller_url, new_key)
| Field | Type | Description |
|---|---|---|
needs_reauth |
bool |
True if the response was HTTP 401 |
seller_url |
str |
Base URL of the seller that returned the error |
status_code |
int |
HTTP status code from the response |
403 is not re-auth
Only HTTP 401 (authentication failure) triggers needs_reauth. HTTP 403 (authorization / insufficient permissions) is intentionally excluded --- it means the key is valid but the buyer lacks access to the requested resource.
Multi-Seller Credential Management¶
The ApiKeyStore is designed for buyers that work with multiple sellers simultaneously. Each seller URL maps to its own API key, and the AuthMiddleware automatically selects the correct key based on the request URL.
store = ApiKeyStore()
# Register keys for multiple sellers
store.add_key("http://seller-sports.example.com:8001", "sk-sports-key")
store.add_key("http://seller-news.example.com:8001", "sk-news-key")
store.add_key("http://seller-entertainment.example.com:8001", "sk-ent-key")
middleware = AuthMiddleware(key_store=store)
# Each request automatically gets the right key
sports_req = httpx.Request("GET", "http://seller-sports.example.com:8001/api/v1/products")
news_req = httpx.Request("GET", "http://seller-news.example.com:8001/api/v1/products")
sports_auth = middleware.add_auth(sports_req) # X-Api-Key: sk-sports-key
news_auth = middleware.add_auth(news_req) # X-Api-Key: sk-news-key
URL Normalization¶
The store strips trailing slashes to ensure consistent key lookup. These all resolve to the same key:
store.add_key("http://seller.example.com:8001/", "sk-key")
store.get_key("http://seller.example.com:8001") # "sk-key"
store.get_key("http://seller.example.com:8001/") # "sk-key"
Key Acquisition Workflow¶
Before the buyer can authenticate to a seller, it needs to obtain an API key. The seller's /auth/api-keys endpoint issues keys with identity fields that determine the buyer's access tier.
sequenceDiagram
participant Buyer as Buyer Agent
participant Store as ApiKeyStore
participant Seller as Seller Agent
Note over Buyer: Step 1: Create key on seller
Buyer->>Seller: POST /auth/api-keys (identity fields)
Seller-->>Buyer: { api_key: "sk-...", key_id: "..." }
Note over Buyer: Step 2: Store the key locally
Buyer->>Store: add_key(seller_url, api_key)
Store-->>Buyer: Persisted to ~/.ad_buyer/seller_keys.json
Note over Buyer: Step 3: Use key in subsequent requests
Buyer->>Seller: GET /api/v1/products (X-Api-Key: sk-...)
Seller-->>Buyer: Full product catalog (tier-appropriate data)
Step 1: Request a Key from the Seller¶
curl -X POST http://seller.example.com:8001/auth/api-keys \
-H "Content-Type: application/json" \
-d '{
"seat_id": "seat-acme-001",
"seat_name": "Acme DSP",
"agency_id": "agency-mega",
"agency_name": "Mega Agency",
"advertiser_id": "adv-widget-co",
"advertiser_name": "Widget Co",
"label": "Widget Co production key",
"expires_in_days": 365
}'
Save the key immediately
The seller returns the full API key only once in the creation response. It cannot be retrieved later. Store it immediately using ApiKeyStore.add_key().
Step 2: Store the Key¶
store = ApiKeyStore()
store.add_key("http://seller.example.com:8001", "sk-returned-key-from-seller")
Step 3: Verify Access¶
# Test that the key works and check your tier
curl -H "X-Api-Key: sk-returned-key-from-seller" \
http://seller.example.com:8001/api/v1/products
A successful response with exact pricing (not just ranges) confirms the key is working and the buyer has been assigned an appropriate tier.
Key Rotation¶
When a key expires or is compromised:
- Create a new key on the seller (
POST /auth/api-keys) - Rotate the local key:
store.rotate_key(seller_url, new_key) - Optionally revoke the old key on the seller (
DELETE /auth/api-keys/{key_id})
# Rotate after receiving a 401
auth_result = middleware.handle_response(response)
if auth_result.needs_reauth:
# 1. Get a new key from the seller
new_key = await create_seller_api_key(auth_result.seller_url)
# 2. Update the local store
store.rotate_key(auth_result.seller_url, new_key)
# 3. Retry the failed request (middleware will use the new key)
retry_request = middleware.add_auth(original_request)
response = await client.send(retry_request)
DealsClient Authentication¶
The DealsClient accepts credentials directly in its constructor and attaches them to every request. This is the simplest path for buyers that interact with a single seller per client instance.
from ad_buyer.clients.deals_client import DealsClient
# API key authentication (sent as X-Api-Key header)
client = DealsClient(
seller_url="http://seller.example.com:8001",
api_key="sk-abc123secret",
)
# Bearer token authentication (sent as Authorization: Bearer header)
client = DealsClient(
seller_url="http://seller.example.com:8001",
bearer_token="eyJhbGciOiJIUzI1NiIs...",
)
One auth method per client
Provide either api_key or bearer_token, not both. If both are set, api_key takes precedence and is sent as the X-Api-Key header.
For multi-seller scenarios, use ApiKeyStore + AuthMiddleware to manage credentials centrally, or create one DealsClient per seller with its own key.
Related¶
- Seller Authentication --- Seller-side key management, access tiers, and trust levels
- Deals API --- Deal client that uses these auth mechanisms
- Seller Discovery --- Discovering sellers to authenticate with
- Identity Strategy --- How buyer identity maps to seller tiers