API Key Management for AI Agent Platforms: Generation, Rotation, and Revocation
Build a production-grade API key management system for AI agent platforms. Covers key generation, secure hashing, scoping, rate limiting, rotation strategies, and revocation with FastAPI.
Why API Keys Still Matter
Despite OAuth2 and JWTs dominating modern authentication, API keys remain the most common way developers interact with AI platforms. OpenAI, Anthropic, Google, and every major AI provider use API keys as the primary access mechanism. The reason is simplicity — a developer copies a key, sets it in an environment variable, and starts making requests. No redirect flows, no browser required.
For AI agent platforms, API keys serve a dual purpose: they authenticate programmatic access from scripts, SDKs, and CI/CD pipelines, and they provide a natural unit for rate limiting, billing, and usage tracking. Getting key management right is critical for both security and developer experience.
Designing the Key Format
A well-designed API key should be immediately identifiable, sufficiently random, and structured for efficient validation. Follow the pattern used by major providers:
csa_live_7f3k9m2x4p8q1w6y0t5r
└──┘ └──┘ └──────────────────┘
prefix env random component
The prefix csa_ (CallSphere Agent) immediately identifies the key source. The environment segment distinguishes live from test keys. The random component provides 128+ bits of entropy.
# auth/api_keys.py
import secrets
import hashlib
from datetime import datetime, timezone
def generate_api_key(environment: str = "live") -> tuple[str, str]:
"""Generate an API key and its hash. Returns (plain_key, key_hash)."""
random_part = secrets.token_urlsafe(24) # 192 bits of entropy
prefix = f"csa_{environment}_"
plain_key = f"{prefix}{random_part}"
# Only store the hash — never the plain key
key_hash = hashlib.sha256(plain_key.encode()).hexdigest()
return plain_key, key_hash
def hash_api_key(plain_key: str) -> str:
"""Hash a key for lookup. Same algorithm as generation."""
return hashlib.sha256(plain_key.encode()).hexdigest()
The critical principle: never store the plain-text key. Show it to the user exactly once at creation time, store only the SHA-256 hash, and use the hash for all lookups. This mirrors how password hashing works — if the database is compromised, the attacker gets hashes, not usable keys.
Database Schema for Key Management
Store keys with their metadata, scopes, and rate limit configuration:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
from sqlalchemy import Column, String, DateTime, Integer, Boolean, JSON
from sqlalchemy.dialects.postgresql import UUID
import uuid
class APIKey(Base):
__tablename__ = "api_keys"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(String, nullable=False, index=True)
org_id = Column(String, nullable=False, index=True)
key_hash = Column(String(64), unique=True, nullable=False, index=True)
key_prefix = Column(String(20), nullable=False) # For display: "csa_live_7f3k..."
name = Column(String(100), nullable=False) # Human-readable label
scopes = Column(JSON, default=list) # ["agents:read", "agents:execute"]
rate_limit_rpm = Column(Integer, default=60) # Requests per minute
is_active = Column(Boolean, default=True)
last_used_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
revoked_at = Column(DateTime(timezone=True), nullable=True)
Key Validation Middleware
Build a FastAPI dependency that extracts the API key from the header, hashes it, looks it up, and enforces rate limits:
from fastapi import Depends, HTTPException, Security, status
from fastapi.security import APIKeyHeader
import time
api_key_header = APIKeyHeader(name="X-API-Key")
# Simple in-memory rate limiter (use Redis in production)
rate_limit_store: dict[str, list[float]] = {}
async def validate_api_key(
key: str = Security(api_key_header),
) -> APIKey:
key_hash = hash_api_key(key)
api_key = await db.get_by_hash(key_hash)
if not api_key or not api_key.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or revoked API key",
)
if api_key.expires_at and api_key.expires_at < datetime.now(timezone.utc):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="API key has expired",
)
# Rate limiting check
now = time.time()
window = rate_limit_store.setdefault(key_hash, [])
window[:] = [t for t in window if now - t < 60] # 1-minute window
if len(window) >= api_key.rate_limit_rpm:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Rate limit exceeded",
)
window.append(now)
# Update last_used_at asynchronously
await db.update_last_used(api_key.id)
return api_key
Key Rotation Without Downtime
Rotation is essential — keys get leaked in logs, screenshots, and shared repositories. Support overlap periods where both old and new keys work:
@router.post("/api-keys/{key_id}/rotate")
async def rotate_key(key_id: str, user=Depends(get_current_user)):
old_key = await db.get_key(key_id)
if not old_key or old_key.user_id != user.sub:
raise HTTPException(status_code=404, detail="Key not found")
# Generate new key
plain_key, key_hash = generate_api_key()
# Create new key with same scopes and limits
new_key = await db.create_key(
user_id=user.sub, org_id=old_key.org_id,
key_hash=key_hash, key_prefix=plain_key[:16] + "...",
name=f"{old_key.name} (rotated)",
scopes=old_key.scopes, rate_limit_rpm=old_key.rate_limit_rpm,
)
# Schedule old key deactivation (grace period)
await db.schedule_revocation(
old_key.id,
revoke_at=datetime.now(timezone.utc) + timedelta(hours=24),
)
return {
"new_key": plain_key, # Show once
"old_key_expires": "24 hours",
"message": "Update your systems, then the old key will auto-expire",
}
Revocation
Immediate revocation should be a single database update that sets is_active = False and records the revocation timestamp. The validation middleware already checks is_active on every request, so the key becomes unusable immediately.
FAQ
Why hash API keys with SHA-256 instead of bcrypt?
API keys are high-entropy random strings, not human-chosen passwords. They do not need the slow hashing that bcrypt provides to resist dictionary attacks. SHA-256 is fast enough for per-request validation while being irreversible — if the database leaks, an attacker cannot recover the original key from the hash. Bcrypt would add significant latency to every API call.
How should I scope API keys for different agent capabilities?
Design scopes around your resource model: agents:read, agents:execute, tools:invoke, logs:read. Let users select scopes during key creation. Enforce scopes in your middleware the same way you enforce JWT scopes. The principle of least privilege applies — a key for reading logs should never be able to execute agents.
What is the recommended key expiration policy?
For production AI agent platforms, require keys to expire within 90 days. Send email notifications at 30, 14, and 7 days before expiry. Provide a rotation endpoint that creates a new key and gives a 24-hour grace period for the old one. Keys used in CI/CD pipelines should have shorter lifetimes and be rotated automatically by the pipeline tooling.
#APIKeys #Security #FastAPI #AIAgents #RateLimiting #KeyManagement #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.