By Sagar Shankaran, Founder of CallSphere
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.
Key takeaways
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.
A well-designed API key should be immediately identifiable, sufficiently random, and structured for efficient validation. Follow the pattern used by major providers:
flowchart LR
CLIENT(["Client SDK"])
GW["API Gateway<br/>auth plus rate limit"]
APP["FastAPI app<br/>handlers and DI"]
VAL["Pydantic validation"]
SVC["Service layer<br/>business logic"]
DB[(Database)]
QUEUE[(Background queue)]
OBS[(Tracing)]
CLIENT --> GW --> APP --> VAL --> SVC
SVC --> DB
SVC --> QUEUE
SVC --> OBS
SVC --> CLIENT
style GW fill:#4f46e5,stroke:#4338ca,color:#fff
style APP fill:#f59e0b,stroke:#d97706,color:#1f2937
style DB fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
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.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
# 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.
Store keys with their metadata, scopes, and rate limit configuration:
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)
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
Rotation is essential — keys get leaked in logs, screenshots, and shared repositories. Support overlap periods where both old and new keys work:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
@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",
}
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.
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.
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.
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

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
A founder's guide to the personal AI assistant market: best AI assistant apps, business-grade options, and how CallSphere's voice agent fits in.
A founder's guide to free AI agents, low-code AI agent builders, and how to know when you should pay for a real platform like CallSphere.
Graphiti is the open-source temporal knowledge graph for AI agents in 2026. Learn how bi-temporal memory beats vector RAG for voice agents and long-running LLMs.
Chatbot app vs ChatGPT in 2026: a founder's clear take on the difference, when to use which, and how a real AI chatbot app development works.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
Head-to-head: OpenAI Frontier and Anthropic's managed agent stack — strengths, fit, and what each means for enterprise AI voice and chat deployment.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco