API Key Authentication Best Practices for Document APIs
API keys are the most common authentication mechanism for document generation APIs. They are simple to implement, easy for developers to use, and work well across every programming language and HTTP client. However, their simplicity hides real security risks. A leaked or poorly managed API key can expose your entire document pipeline, leaking sensitive templates, customer data, and billing credentials.
This guide covers four pillars of API key security that every document API should implement: SHA-256 hashing for storage, automated key rotation, intelligent rate limiting, and HMAC-based webhook verification.
1. SHA-256 Hashing: Never Store Keys in Plain Text
The cardinal rule of API key management is to never store the raw key in your database. Treat API keys with the same respect you give passwords. When a user generates a new key, show it to them exactly once, then store only a SHA-256 hash.
import hashlib
def hash_api_key(raw_key: str) -> str:
return hashlib.sha256(raw_key.encode('utf-8')).hexdigest()
def verify_api_key(raw_key: str, stored_hash: str) -> bool:
return hash_api_key(raw_key) == stored_hash
SHA-256 is preferred over bcrypt for API keys because keys are long, random strings (not human-chosen passwords), so brute-force attacks are already impractical. The speed of SHA-256 means authentication adds negligible latency to each API call.
Key Prefix Strategy
Store a non-secret prefix (e.g., the first 8 characters) alongside the hash. This lets you look up the key in the database without scanning every row. At Doxnex, keys follow the format dxn_live_xxxxxxxxxxxxxxxxxxxxxxxx where the prefix dxn_live_ identifies the key type and environment.
2. Key Rotation: Automate the Lifecycle
Keys should not live forever. A 90-day rotation policy balances security with operational convenience. The most important feature to build is a grace period: when a user generates a new key, the old key continues to work for a configurable window (typically 24 to 72 hours).
- Notify before expiry — Send email alerts at 14 days, 7 days, and 1 day before a key expires.
- Support multiple active keys — Allow at least two active keys per account so consumers can deploy the new key before revoking the old one.
- Log rotation events — Record who rotated the key, when, and from which IP address for audit trails.
- Revocation is immediate — When a key is explicitly revoked (as opposed to rotated), it must stop working instantly. Cache invalidation is critical here.
Implementation Pattern
Use a database table with columns for key_hash, prefix, created_at, expires_at, revoked_at, and account_id. On each request, look up the key by prefix, verify the hash, and check that revoked_at is null and expires_at is in the future.
3. Rate Limiting: Protect Your Infrastructure
Document generation is CPU and memory intensive. A single PDF render can consume 100-500 MB of memory depending on template complexity. Without rate limiting, a single runaway client can bring down your entire cluster.
Sliding Window Algorithm
The sliding window counter, backed by Redis, offers the best balance of accuracy and performance. Unlike fixed windows, it prevents burst attacks at window boundaries.
-- Redis sliding window rate limiter
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. math.random())
redis.call('EXPIRE', key, window)
return 1
end
return 0
Tiered Limits
Different subscription plans should have different limits. A practical starting point for document APIs:
- Free tier: 10 requests/minute, 100 documents/day
- Starter: 60 requests/minute, 1,000 documents/day
- Business: 300 requests/minute, 10,000 documents/day
- Enterprise: Custom limits with dedicated infrastructure
Always return rate limit headers in every response: X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. When a client is throttled, return HTTP 429 with a Retry-After header.
4. HMAC Webhook Verification
When your document API sends webhooks (e.g., "document generation complete" or "export ready for download"), the receiver must verify that the webhook genuinely came from your service. HMAC-SHA256 is the industry standard for this.
import hmac
import hashlib
def sign_webhook(payload: bytes, secret: str) -> str:
return hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = sign_webhook(payload, secret)
return hmac.compare_digest(expected, signature)
Key implementation details:
- Use
hmac.compare_digest(or equivalent) for constant-time comparison. A naive==comparison is vulnerable to timing attacks. - Include a timestamp in the signed payload and reject webhooks older than 5 minutes to prevent replay attacks.
- Provide a unique webhook secret per endpoint, separate from the API key. If one webhook URL is compromised, others remain secure.
5. Additional Security Layers
IP Allowlisting
For enterprise customers, allow restricting API key usage to specific IP ranges. This adds defense-in-depth: even if a key leaks, it cannot be used from unauthorized networks.
Scope Restrictions
Not every key needs full access. Implement scoped keys that can only perform specific operations. For a document API, useful scopes include documents:generate, templates:read, templates:write, and billing:read.
Audit Logging
Log every API key usage event with the timestamp, endpoint, response status, and client IP. This data is invaluable for detecting anomalies (sudden spike in requests, access from unusual geographies) and for compliance requirements.
Putting It All Together
A secure API key system is not a single feature but a layered architecture. Hash keys at rest, enforce rotation policies, protect infrastructure with rate limits, and verify webhooks with HMAC signatures. Each layer compensates for potential failures in the others.
At Doxnex, we implement all of these practices in our document generation API. Every key is SHA-256 hashed, rotation is enforced and automated, rate limits scale with your plan, and all webhooks are HMAC-signed with per-endpoint secrets. Get started with a free account and see these practices in action.
Frequently Asked Questions
Should I store API keys in plain text?
Never store API keys in plain text. Always store a SHA-256 hash of the key in your database. When a request arrives, hash the provided key and compare it against the stored hash. This way, even if your database is compromised, the actual keys remain safe.
How often should I rotate API keys?
Best practice is to rotate API keys every 90 days. Support a grace period where both old and new keys work simultaneously so consumers can migrate without downtime. Automated rotation with advance notification is ideal for production systems.
What rate limits should I set for a document generation API?
For document generation APIs, a reasonable default is 60 requests per minute for standard tiers and 300 per minute for premium tiers. Use a sliding window algorithm backed by Redis for accurate counting. Always return X-RateLimit headers so consumers can self-throttle.
How does HMAC webhook verification work?
HMAC webhook verification uses a shared secret between the sender and receiver. The sender computes an HMAC-SHA256 signature of the request body using the secret and includes it in a header. The receiver recomputes the signature and compares it using a constant-time comparison to prevent timing attacks.