Skip to content
Learn Agentic AI14 min read0 views

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.

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

Share this article
C

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.