API Security & Rate Limiting Implementation Workflow
The modern API economy powers 83% of internet traffic, but this growth comes with unprecedented security challenges. Organizations now expose an average of 15,000+ API endpoints, creating a massive attack surface. API attacks have increased 400% year-over-year, with the average breach costing $4.5 million. This comprehensive workflow provides a systematic approach to implementing production-grade API security that protects your services, satisfies compliance requirements, and enables sustainable business growth.
Introduction
The API Security Challenge in 2025
APIs have become the backbone of digital business, enabling everything from mobile app functionality to complex microservice architectures. However, this central role makes them prime targets for attackers. The consequences of inadequate API security extend far beyond technical failures:
Business Impact:
- Regulatory penalties from GDPR, PSD2, CCPA, HIPAA violations
- Loss of customer trust and brand reputation damage
- Service disruptions affecting revenue and user experience
- Competitive disadvantage from security incidents
Technical Challenges:
- Broken authentication and authorization (OWASP API1, API2)
- Inadequate rate limiting enabling abuse and DDoS attacks
- Insecure data exposure through over-permissive APIs
- Lack of visibility into API usage and security events
Why This Workflow Matters
This workflow addresses the complete lifecycle of API security implementation, from initial authentication design through production monitoring and incident response. Unlike fragmented security approaches, this systematic methodology ensures:
Secure by Default: APIs implement security controls from day one, reducing security debt by 60% compared to retroactive hardening.
Compliance Ready: Built-in support for PSD2, SOC 2, HIPAA, and other regulatory frameworks through comprehensive logging, access controls, and audit trails.
Business Enablement: Rate limiting and quota systems support API monetization strategies, turning security controls into revenue generators.
Operational Excellence: Monitoring and incident response playbooks enable rapid threat detection and remediation.
Workflow Overview
This guide presents a 6-stage approach covering all critical aspects of API security:
Stage 1: Authentication & Authorization Architecture (2-4 hours) Implement OAuth 2.1/OIDC with PKCE, design secure JWT tokens, establish authorization policies including object-level access controls.
Stage 2: Rate Limiting Strategy & Implementation (2-3 hours) Select optimal algorithms (token bucket, sliding window), configure tiered limits, implement quota management and overage handling.
Stage 3: API Gateway Security & CORS Configuration (1.5-3 hours) Deploy centralized security controls, configure security headers, establish CORS policies for cross-origin access.
Stage 4: Webhook Security Implementation (1.5-3 hours) Validate HMAC signatures, prevent replay attacks, implement delivery guarantees and retry logic.
Stage 5: API Monetization & Quota Enforcement (2-3 hours) Design tiered pricing structures, track usage in real-time, handle overages and upgrade flows.
Stage 6: Monitoring, Logging & Incident Response (2-3 hours) Establish comprehensive observability, configure threat detection, create incident response playbooks.
The total implementation time ranges from 11-19 hours, with the specific duration depending on API complexity and organizational requirements.
Stage 1: Authentication & Authorization Architecture (2-4 hours)
Authentication verifies who users are, while authorization determines what they can access. Modern API security requires both layers implemented according to current best practices.
Step 1.1: OAuth/OIDC Flow Selection & Design (30-60 minutes)
OAuth 2.1 and OpenID Connect (OIDC) provide industry-standard frameworks for API authentication. As of January 2025, the IETF published RFC 9700 (OAuth 2.0 Security Best Current Practice), which mandates significant changes from OAuth 2.0.
Critical Changes in OAuth 2.1:
The Authorization Code flow with PKCE (Proof Key for Code Exchange) is now required for ALL client types, including confidential clients that previously could skip PKCE. The Implicit grant and Resource Owner Password Credentials (ROPC) flows are deprecated and removed from OAuth 2.1 due to security vulnerabilities.
Authorization Code + PKCE Flow:
This flow provides the strongest security guarantees and works across all application types:
// Step 1: Generate PKCE challenge (client-side)
function generatePKCE() {
// Create cryptographically random code verifier (43-128 characters)
const codeVerifier = base64URLEncode(crypto.getRandomValues(new Uint8Array(32)));
// Generate code challenge (SHA-256 hash of verifier)
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const hash = await crypto.subtle.digest('SHA-256', data);
const codeChallenge = base64URLEncode(new Uint8Array(hash));
return { codeVerifier, codeChallenge };
}
// Step 2: Build authorization request
function buildAuthorizationRequest(codeChallenge) {
const params = new URLSearchParams({
response_type: 'code',
client_id: 'your_client_id',
redirect_uri: 'https://app.example.com/callback',
scope: 'openid profile email read:orders write:orders',
state: generateRandomString(32), // CSRF protection
nonce: generateRandomString(32), // ID token replay protection (OIDC)
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
// Redirect user to authorization endpoint
window.location.href = `https://auth.example.com/authorize?${params}`;
}
// Step 3: Exchange authorization code for tokens (server-side)
async function exchangeCodeForTokens(code, codeVerifier) {
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://app.example.com/callback',
code_verifier: codeVerifier // PKCE verification
})
});
const tokens = await response.json();
// Returns: { access_token, refresh_token, id_token, expires_in, token_type }
return tokens;
}
Client Credentials Flow (Server-to-Server):
For machine-to-machine authentication where no user is involved:
async function getClientCredentialsToken() {
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'read:data write:data'
})
});
const { access_token, expires_in } = await response.json();
return access_token;
}
Device Authorization Grant (IoT/CLI):
For devices with limited input capabilities:
// Step 1: Request device code
async function initiateDeviceFlow() {
const response = await fetch('https://auth.example.com/device/code', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: 'your_client_id',
scope: 'openid profile device:control'
})
});
const data = await response.json();
// {
// device_code: "NGU5OWFiNjQ...",
// user_code: "WDJB-MJHT",
// verification_uri: "https://auth.example.com/device",
// expires_in: 1800,
// interval: 5
// }
console.log(`Visit ${data.verification_uri} and enter code: ${data.user_code}`);
return data;
}
// Step 2: Poll for authorization
async function pollForToken(deviceCode, interval) {
while (true) {
await sleep(interval * 1000);
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: deviceCode,
client_id: 'your_client_id'
})
});
if (response.ok) {
return await response.json();
} else if (response.status === 400) {
const error = await response.json();
if (error.error === 'authorization_pending') {
continue; // Keep polling
} else if (error.error === 'slow_down') {
interval += 5; // Increase polling interval
continue;
} else {
throw new Error(error.error);
}
}
}
}
Security Best Practices:
- Always use HTTPS for all OAuth endpoints and redirects
- Implement exact redirect URI matching (no wildcards or partial matches)
- Use state parameter with 32+ random characters for CSRF prevention
- Include nonce parameter in OIDC requests to prevent ID token replay
- Never send tokens in URL query parameters (use Authorization header)
- Store client secrets securely (environment variables, secret managers)
You can design and test these flows using the OAuth/OIDC Debugger to generate PKCE challenges, validate authorization requests, and verify flow compliance with RFC 9700.
Step 1.2: JWT Token Design & Validation (45-90 minutes)
JSON Web Tokens (JWTs) are the standard format for API access tokens. Proper JWT design and validation prevents numerous security vulnerabilities.
JWT Structure:
A JWT consists of three Base64URL-encoded parts separated by periods:
header.payload.signature
Example Access Token:
// Header
{
"alg": "RS256", // Algorithm (RS256 recommended)
"typ": "at+jwt", // Token type (RFC 9068)
"kid": "key-2025-01" // Key ID for rotation
}
// Payload
{
// Standard claims
"iss": "https://auth.example.com", // Issuer
"sub": "usr_123456", // Subject (user ID)
"aud": "https://api.example.com", // Audience (API identifier)
"exp": 1704824400, // Expiration (15 min from now)
"iat": 1704823500, // Issued at
"nbf": 1704823500, // Not before
"jti": "jti_abc123def", // JWT ID (unique identifier)
// Custom claims
"scope": "read:orders write:orders",
"roles": ["customer", "api_consumer"],
"tenant_id": "tenant_xyz",
"subscription_tier": "pro"
}
// Signature (computed from header + payload)
Signature Algorithms:
RS256 (Recommended): Asymmetric encryption using RSA keys. Public key distributed for verification, private key kept secure for signing. Ideal for distributed systems where multiple services need to validate tokens.
HS256 (Acceptable): Symmetric encryption using shared secret. Same key used for signing and verification. Suitable for single-service architectures where key distribution is controlled.
ES256 (Emerging): Asymmetric encryption using ECDSA. Smaller key size than RS256 with equivalent security. Growing adoption for performance-sensitive applications.
Production JWT Validation:
import jwt from 'jsonwebtoken';
import { JwksClient } from 'jwks-rsa';
// Middleware for JWT validation
async function validateJWT(request) {
// 1. Extract token from Authorization header
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedError('Missing or invalid Authorization header');
}
const token = authHeader.substring(7);
// 2. Decode header without verification (to get kid)
const decodedHeader = jwt.decode(token, { complete: true });
if (!decodedHeader) {
throw new UnauthorizedError('Invalid token format');
}
// 3. Verify algorithm is expected (prevent algorithm confusion attacks)
if (decodedHeader.header.alg !== 'RS256') {
throw new UnauthorizedError('Unsupported algorithm');
}
// 4. Fetch public key from JWKS endpoint (with caching)
const jwksClient = new JwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
cacheMaxAge: 86400000, // 24 hours
rateLimit: true
});
const kid = decodedHeader.header.kid;
const key = await jwksClient.getSigningKey(kid);
const publicKey = key.getPublicKey();
// 5. Verify signature and claims
try {
const payload = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
clockTolerance: 30 // Allow 30 seconds clock skew
});
// 6. Additional validation
if (!payload.sub) {
throw new UnauthorizedError('Missing subject claim');
}
// 7. Verify required scopes
const requiredScopes = ['read:orders'];
const grantedScopes = payload.scope ? payload.scope.split(' ') : [];
const hasRequiredScopes = requiredScopes.every(scope =>
grantedScopes.includes(scope)
);
if (!hasRequiredScopes) {
throw new ForbiddenError('Insufficient scopes');
}
return payload;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new UnauthorizedError('Token expired');
} else if (error.name === 'JsonWebTokenError') {
throw new UnauthorizedError('Invalid token signature');
} else if (error.name === 'NotBeforeError') {
throw new UnauthorizedError('Token not yet valid');
}
throw error;
}
}
Critical Security Validations:
Always reject tokens with alg: "none" - this allows unsigned tokens and is a common attack vector. Verify the aud claim matches your API identifier to prevent token reuse across different services. Enforce short expiration times (15 minutes for access tokens) to limit damage from token leakage. Never trust the exp claim alone - always verify the signature first.
Token Storage Security:
Web Applications: Store tokens in HttpOnly, Secure, SameSite=Strict cookies to prevent XSS attacks. Never use localStorage or sessionStorage for sensitive tokens.
Single Page Apps: Keep access tokens in memory only (JavaScript variables). Use refresh tokens in HttpOnly cookies with backend-for-frontend (BFF) pattern.
Mobile Apps: Use secure platform storage - iOS Keychain or Android KeyStore. Enable biometric authentication for sensitive operations.
Server-to-Server: Store in environment variables or secret management systems (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault).
Use the JWT Decoder to analyze token structure, validate claims, detect security issues, and understand expiration times during development and debugging.
Step 1.3: Authorization Model Implementation (45-90 minutes)
Authentication identifies users, but authorization determines what they can do. Proper authorization prevents OWASP API1 (Broken Object Level Authorization) and API5 (Broken Function Level Authorization).
Scope-Based Authorization:
Scopes provide coarse-grained access control at the endpoint or resource type level:
// Scope naming convention: {action}:{resource}:{qualifier}
const SCOPE_DEFINITIONS = {
// Read permissions
'read:orders': 'View all orders',
'read:orders:own': 'View own orders only',
'read:customers': 'View customer information',
// Write permissions
'write:orders': 'Create and modify orders',
'write:customers': 'Create and modify customers',
// Admin permissions
'admin:orders': 'Full order management including deletions',
'admin:users': 'User management and role assignment',
// Sensitive operations
'refund:orders': 'Issue refunds',
'delete:data': 'Permanent data deletion'
};
// Middleware to enforce scope requirements
function requireScopes(...requiredScopes) {
return async (request, payload) => {
const grantedScopes = payload.scope ? payload.scope.split(' ') : [];
const hasAllScopes = requiredScopes.every(scope =>
grantedScopes.includes(scope)
);
if (!hasAllScopes) {
throw new ForbiddenError(
`Missing required scopes: ${requiredScopes.join(', ')}`
);
}
return true;
};
}
// Usage in API endpoints
app.get('/api/orders',
validateJWT,
requireScopes('read:orders'),
async (req, res) => {
// User has valid token with read:orders scope
const orders = await getOrders();
res.json(orders);
}
);
Role-Based Access Control (RBAC):
Roles group related permissions and simplify permission management:
const ROLE_DEFINITIONS = {
customer: {
scopes: ['read:orders:own', 'write:orders:own', 'read:profile']
},
support: {
scopes: ['read:orders', 'read:customers', 'write:tickets']
},
admin: {
scopes: ['read:*', 'write:*', 'admin:*']
},
api_consumer: {
scopes: ['read:orders', 'read:customers', 'write:orders']
}
};
function hasRole(payload, requiredRole) {
const userRoles = payload.roles || [];
return userRoles.includes(requiredRole);
}
// Check role membership
if (!hasRole(payload, 'admin')) {
throw new ForbiddenError('Admin role required');
}
Object-Level Authorization (Critical for OWASP API1):
Scope and role checks alone are insufficient. You must verify the user has permission to access the SPECIFIC resource:
// ❌ VULNERABLE: No object-level authorization
app.get('/api/orders/:orderId',
validateJWT,
requireScopes('read:orders'),
async (req, res) => {
// User has read:orders scope, but can they access THIS order?
const order = await db.orders.findById(req.params.orderId);
res.json(order); // SECURITY ISSUE: No ownership check
}
);
// ✅ SECURE: Object-level authorization enforced
app.get('/api/orders/:orderId',
validateJWT,
requireScopes('read:orders'),
async (req, res) => {
const order = await db.orders.findById(req.params.orderId);
if (!order) {
throw new NotFoundError('Order not found');
}
// Verify user owns this order OR has admin scope
const userId = req.payload.sub;
const isAdmin = req.payload.scope.includes('admin:orders');
const ownsOrder = order.user_id === userId;
if (!ownsOrder && !isAdmin) {
// Return 404 to prevent information leakage
throw new NotFoundError('Order not found');
}
res.json(order);
}
);
Attribute-Based Access Control (ABAC):
For complex authorization logic based on user attributes, resource properties, and environmental factors:
// Policy evaluation function
function evaluatePolicy(user, resource, action, context) {
// Example: Allow invoice access if user's department matches invoice department
if (action === 'read' && resource.type === 'invoice') {
return user.department === resource.department;
}
// Example: Restrict sensitive operations to office hours
if (action === 'delete' && resource.sensitivity === 'high') {
const hour = new Date().getHours();
const isBusinessHours = hour >= 9 && hour < 17;
return isBusinessHours && user.roles.includes('admin');
}
// Example: Require MFA for financial transactions over $10,000
if (action === 'approve' && resource.type === 'transaction') {
if (resource.amount > 10000) {
return context.mfa_verified === true;
}
}
return false;
}
Rich Authorization Requests (FAPI 2.0):
OAuth 2.0 Rich Authorization Requests (RAR) enable fine-grained authorization:
// Traditional scope-based request
const scopeRequest = {
scope: 'read:orders write:orders'
};
// Rich Authorization Request (RFC 9396)
const rarRequest = {
authorization_details: [
{
type: 'order_access',
actions: ['read', 'write'],
locations: ['https://api.example.com/orders'],
datatypes: ['order', 'shipment'],
identifier: 'order_123456' // Specific resource
},
{
type: 'payment_initiation',
actions: ['create'],
instructedAmount: {
currency: 'USD',
amount: '150.00'
},
creditorAccount: {
iban: 'DE1234567890'
}
}
]
};
Authorization Matrix Example:
Create a clear matrix defining what each role can do:
| Resource | Customer | Support | Admin | API Consumer |
|---|---|---|---|---|
| Own Orders | Read, Create | Read | Read, Update, Delete | Read, Create |
| All Orders | - | Read | Read, Update, Delete | Read |
| Own Profile | Read, Update | - | Read, Update | - |
| All Profiles | - | Read | Read, Update, Delete | - |
| Refunds | - | Create | Create, Approve | - |
| Settings | - | - | Read, Update | - |
This comprehensive authorization approach prevents the most common API vulnerabilities while supporting complex business requirements.
Stage 2: Rate Limiting Strategy & Implementation (2-3 hours)
Rate limiting protects APIs from abuse, ensures fair resource allocation, and enables monetization through tiered pricing. Without rate limiting, a single client can overwhelm your infrastructure or consume disproportionate resources.
Step 2.1: Rate Limit Algorithm Selection (30-60 minutes)
Different algorithms suit different use cases. Understanding their trade-offs enables optimal selection.
Token Bucket Algorithm (Recommended for Most APIs):
The token bucket allows bursts while enforcing long-term rate limits. Tokens refill at a constant rate, and each request consumes one token. When the bucket is empty, requests are rejected.
class TokenBucketRateLimiter {
constructor(capacity, refillRate) {
this.capacity = capacity; // Maximum tokens in bucket
this.tokens = capacity; // Current tokens available
this.refillRate = refillRate; // Tokens per second
this.lastRefill = Date.now();
}
async checkLimit(clientId) {
// Refill tokens based on elapsed time
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000; // seconds
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
// Check if request allowed
if (this.tokens >= 1) {
this.tokens -= 1;
return {
allowed: true,
remaining: Math.floor(this.tokens),
resetAt: new Date(now + ((this.capacity - this.tokens) / this.refillRate) * 1000)
};
}
// Calculate retry after time
const retryAfter = Math.ceil((1 - this.tokens) / this.refillRate);
return {
allowed: false,
remaining: 0,
resetAt: new Date(now + (this.capacity / this.refillRate) * 1000),
retryAfter: retryAfter
};
}
}
// Usage: Allow burst of 100 requests, refill at 10 requests/second
const limiter = new TokenBucketRateLimiter(100, 10);
// Redis-based implementation for distributed systems
async function checkTokenBucket(redis, clientId, capacity, refillRate) {
const key = `ratelimit:${clientId}`;
const now = Date.now();
// Get current state
const state = await redis.get(key);
let tokens, lastRefill;
if (state) {
const parsed = JSON.parse(state);
tokens = parsed.tokens;
lastRefill = parsed.lastRefill;
} else {
tokens = capacity;
lastRefill = now;
}
// Refill tokens
const elapsed = (now - lastRefill) / 1000;
tokens = Math.min(capacity, tokens + (elapsed * refillRate));
if (tokens >= 1) {
tokens -= 1;
// Save state
await redis.setex(key, 3600, JSON.stringify({
tokens: tokens,
lastRefill: now
}));
return { allowed: true, remaining: Math.floor(tokens) };
}
return { allowed: false, remaining: 0 };
}
Sliding Window Algorithm (Fairest Distribution):
Tracks requests in a continuous rolling window, preventing burst exploits at window boundaries:
async function checkSlidingWindow(redis, clientId, limit, windowMs) {
const key = `ratelimit:sliding:${clientId}`;
const now = Date.now();
const windowStart = now - windowMs;
// Redis sorted set: timestamp as score, request ID as value
const pipeline = redis.pipeline();
// Remove expired requests
pipeline.zremrangebyscore(key, '-inf', windowStart);
// Count requests in current window
pipeline.zcard(key);
// Add current request
const requestId = `${now}-${Math.random()}`;
pipeline.zadd(key, now, requestId);
// Set expiration
pipeline.expire(key, Math.ceil(windowMs / 1000) + 1);
const results = await pipeline.exec();
const count = results[1][1]; // Result of zcard
if (count < limit) {
return {
allowed: true,
remaining: limit - count - 1,
resetAt: new Date(now + windowMs)
};
}
// Remove the request we just added since it's rejected
await redis.zrem(key, requestId);
return {
allowed: false,
remaining: 0,
resetAt: new Date(now + windowMs)
};
}
// Usage: 100 requests per 60-second sliding window
const result = await checkSlidingWindow(redis, 'client_123', 100, 60000);
Leaky Bucket Algorithm (Smooth Traffic):
Requests are queued and processed at a steady rate, smoothing traffic bursts:
class LeakyBucketRateLimiter {
constructor(capacity, leakRate) {
this.capacity = capacity; // Queue size
this.leakRate = leakRate; // Requests per second
this.queue = [];
this.processing = false;
}
async addRequest(request) {
if (this.queue.length >= this.capacity) {
throw new RateLimitError('Queue full - request rejected');
}
this.queue.push(request);
if (!this.processing) {
this.processQueue();
}
}
async processQueue() {
this.processing = true;
while (this.queue.length > 0) {
const request = this.queue.shift();
await this.handleRequest(request);
// Wait before processing next request (leak rate)
await sleep(1000 / this.leakRate);
}
this.processing = false;
}
async handleRequest(request) {
// Process the request
}
}
Fixed Window Algorithm (Simplest):
Counts requests per fixed time interval. Simple but prone to burst attacks at window boundaries:
async function checkFixedWindow(redis, clientId, limit, windowSeconds) {
const now = Math.floor(Date.now() / 1000);
const windowStart = Math.floor(now / windowSeconds) * windowSeconds;
const key = `ratelimit:fixed:${clientId}:${windowStart}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, windowSeconds);
}
const resetAt = new Date((windowStart + windowSeconds) * 1000);
if (count <= limit) {
return {
allowed: true,
remaining: limit - count,
resetAt: resetAt
};
}
return {
allowed: false,
remaining: 0,
resetAt: resetAt,
retryAfter: windowSeconds - (now % windowSeconds)
};
}
// Usage: 100 requests per 60-second window
const result = await checkFixedWindow(redis, 'client_123', 100, 60);
Algorithm Comparison:
| Algorithm | Pros | Cons | Best For |
|---|---|---|---|
| Token Bucket | Allows bursts, simple | Can allow large bursts | REST APIs, file uploads |
| Sliding Window | Fairest, no burst edges | Higher memory/CPU | LLM APIs, financial transactions |
| Leaky Bucket | Smooth traffic | Adds latency | Database writes, email sending |
| Fixed Window | Simplest, lowest overhead | Burst vulnerability | Internal APIs, low-security |
Use the Rate Limit Calculator to model different algorithms, calculate optimal limits based on your traffic patterns, and simulate various scenarios.
Step 2.2: Per-Client vs Global Rate Limiting (45-90 minutes)
Effective rate limiting requires both per-client fairness and global backend protection.
Per-Client Rate Limiting Implementation:
// Middleware for per-client rate limiting
async function perClientRateLimit(request, response, next) {
// Identify client (priority order)
const apiKey = request.headers.get('X-API-Key');
const userId = request.payload?.sub;
const ipAddress = request.ip;
const clientId = apiKey || userId || ipAddress;
// Get client's tier and limits
const tier = await getClientTier(clientId);
const limits = TIER_LIMITS[tier];
// Check rate limit
const result = await checkTokenBucket(
redis,
clientId,
limits.burstCapacity,
limits.refillRate
);
// Add rate limit headers
response.setHeader('X-RateLimit-Limit', limits.burstCapacity);
response.setHeader('X-RateLimit-Remaining', result.remaining);
response.setHeader('X-RateLimit-Reset', result.resetAt.toISOString());
if (!result.allowed) {
response.setHeader('Retry-After', result.retryAfter);
return response.status(429).json({
error: 'rate_limit_exceeded',
message: `Rate limit of ${limits.burstCapacity} requests per minute exceeded`,
retry_after: result.retryAfter,
limit: limits.burstCapacity,
remaining: 0,
reset_at: result.resetAt.toISOString()
});
}
next();
}
Tiered Rate Limits:
const TIER_LIMITS = {
free: {
burstCapacity: 20, // 20 request burst
refillRate: 10 / 60, // 10 per minute
dailyQuota: 1000,
endpointLimits: {
'POST /auth/login': { capacity: 5, rate: 5 / 60 } // Stricter for auth
}
},
builder: {
burstCapacity: 100,
refillRate: 60 / 60, // 60 per minute
dailyQuota: 100000,
endpointLimits: {
'POST /auth/login': { capacity: 10, rate: 10 / 60 }
}
},
pro: {
burstCapacity: 1000,
refillRate: 600 / 60, // 600 per minute
dailyQuota: 1000000,
endpointLimits: {} // No special restrictions
},
enterprise: {
burstCapacity: 10000,
refillRate: 6000 / 60, // 6000 per minute
dailyQuota: null, // Unlimited
endpointLimits: {}
}
};
Endpoint-Specific Limits:
Different endpoints have different resource costs and security requirements:
const ENDPOINT_LIMITS = {
// Authentication (brute force protection)
'POST /auth/login': { capacity: 5, rate: 5 / 60 },
'POST /auth/forgot-password': { capacity: 3, rate: 3 / 300 },
// Read operations (generous)
'GET /users/:id': { capacity: 100, rate: 100 / 60 },
'GET /orders': { capacity: 100, rate: 100 / 60 },
// Write operations (moderate)
'POST /orders': { capacity: 20, rate: 20 / 60 },
'PUT /users/:id': { capacity: 10, rate: 10 / 60 },
// Expensive operations (restrictive)
'POST /search': { capacity: 10, rate: 10 / 60 },
'POST /reports': { capacity: 5, rate: 5 / 300 },
// Sensitive operations (very restrictive)
'DELETE /users/:id': { capacity: 1, rate: 1 / 60 },
'POST /refunds': { capacity: 5, rate: 5 / 300 }
};
async function getEndpointLimit(method, path, tier) {
const endpointKey = `${method} ${path}`;
const endpointLimit = ENDPOINT_LIMITS[endpointKey];
const tierLimit = TIER_LIMITS[tier];
// Use endpoint-specific limit if defined, otherwise tier default
return endpointLimit || {
capacity: tierLimit.burstCapacity,
rate: tierLimit.refillRate
};
}
Global Rate Limiting (Backend Protection):
// Global rate limiter to protect backend
async function globalRateLimit(request, response, next) {
const globalLimit = 10000; // 10,000 requests per second across all clients
const result = await checkTokenBucket(redis, 'global', globalLimit, globalLimit);
if (!result.allowed) {
// Backend overloaded - activate circuit breaker
response.setHeader('Retry-After', 60);
return response.status(503).json({
error: 'service_unavailable',
message: 'Service temporarily overloaded, please retry',
retry_after: 60
});
}
next();
}
Rate Limit Response Headers:
Standard headers provide clients with limit information:
function setRateLimitHeaders(response, limit, remaining, resetAt, retryAfter = null) {
response.setHeader('X-RateLimit-Limit', limit.toString());
response.setHeader('X-RateLimit-Remaining', remaining.toString());
response.setHeader('X-RateLimit-Reset', Math.floor(resetAt.getTime() / 1000).toString());
if (retryAfter !== null) {
response.setHeader('Retry-After', retryAfter.toString());
}
// Optional: Add rate limit policy header
response.setHeader('X-RateLimit-Policy', `${limit} requests per minute`);
}
Step 2.3: Quota Management & Overage Handling (45-90 minutes)
Quotas enforce monthly or billing-period limits, distinct from per-minute rate limits.
Monthly Quota Tracking:
async function trackMonthlyUsage(redis, apiKey) {
const month = new Date().toISOString().slice(0, 7); // "2025-01"
const key = `quota:${apiKey}:${month}`;
// Increment usage
const currentUsage = await redis.incr(key);
// Set expiration (90 days retention)
if (currentUsage === 1) {
await redis.expire(key, 90 * 86400);
}
// Get quota limit for this client
const quota = await getQuotaLimit(apiKey);
const percentageUsed = (currentUsage / quota) * 100;
// Send alerts at thresholds
if (percentageUsed >= 100 && currentUsage === quota + 1) {
await sendQuotaAlert(apiKey, '100', currentUsage, quota);
} else if (percentageUsed >= 90 && Math.floor((currentUsage - 1) / quota * 100) < 90) {
await sendQuotaAlert(apiKey, '90', currentUsage, quota);
} else if (percentageUsed >= 80 && Math.floor((currentUsage - 1) / quota * 100) < 80) {
await sendQuotaAlert(apiKey, '80', currentUsage, quota);
}
return { currentUsage, quota, percentageUsed };
}
Quota Enforcement Strategies:
async function enforceQuota(apiKey, currentUsage, quota) {
if (currentUsage <= quota) {
return { allowed: true };
}
// Get overage strategy for client's tier
const tier = await getClientTier(apiKey);
const strategy = OVERAGE_STRATEGIES[tier];
switch (strategy.type) {
case 'hard_stop':
return {
allowed: false,
status: 402,
error: 'quota_exceeded',
message: `Monthly quota of ${quota} requests exhausted`,
reset_at: getNextBillingCycle(),
upgrade_url: `https://api.example.com/upgrade?api_key=${apiKey}`
};
case 'soft_quota':
// Allow continued use but throttle rate limit
const throttledLimit = strategy.throttledRate;
return {
allowed: true,
throttled: true,
throttledLimit: throttledLimit,
warning: `Quota exceeded, rate limited to ${throttledLimit} requests/minute`
};
case 'overage_billing':
// Charge per additional request
const overageRequests = currentUsage - quota;
const overageCharge = overageRequests * strategy.pricePerRequest;
await recordOverageCharge(apiKey, overageRequests, overageCharge);
return {
allowed: true,
overage: true,
overageRequests: overageRequests,
overageCharge: overageCharge,
warning: `${overageRequests} requests beyond quota ($${overageCharge.toFixed(2)})`
};
default:
return { allowed: false };
}
}
const OVERAGE_STRATEGIES = {
free: {
type: 'hard_stop'
},
builder: {
type: 'soft_quota',
throttledRate: 10 // Reduce to 10 requests/minute
},
pro: {
type: 'overage_billing',
pricePerRequest: 0.001 // $0.001 per request
},
enterprise: {
type: 'unlimited' // No quotas
}
};
Quota Alert Webhooks:
async function sendQuotaAlert(apiKey, threshold, currentUsage, quota) {
const customer = await getCustomer(apiKey);
const webhookPayload = {
event: threshold === '100' ? 'quota.exhausted' : 'quota.warning',
api_key: apiKey,
customer_id: customer.id,
usage_percentage: parseInt(threshold),
current_usage: currentUsage,
quota_limit: quota,
period_start: getMonthStart().toISOString(),
period_end: getMonthEnd().toISOString(),
recommended_action: threshold === '100' ? 'upgrade_required' : 'consider_upgrade'
};
// Send webhook to customer's configured endpoint
if (customer.webhookUrl) {
await sendWebhook(customer.webhookUrl, webhookPayload, customer.webhookSecret);
}
// Send email notification
await sendEmail(customer.email, 'quota-alert', webhookPayload);
}
Test quota webhook delivery and validation using the Webhook Tester & Inspector.
Stage 3: API Gateway Security & CORS Configuration (1.5-3 hours)
API gateways centralize security controls, reducing duplicated effort across services and ensuring consistent policy enforcement.
Step 3.1: API Gateway Selection & Configuration (45-90 minutes)
Modern API gateways provide authentication, rate limiting, request validation, and observability as managed services.
Kong Gateway Configuration:
# kong.yml - Declarative configuration
_format_version: "3.0"
services:
- name: orders-api
url: http://backend:8080
routes:
- name: orders-route
paths:
- /api/orders
methods:
- GET
- POST
- PUT
- DELETE
plugins:
- name: jwt
config:
key_claim_name: kid
secret_is_base64: false
claims_to_verify:
- exp
- nbf
uri_param_names:
- jwt
- name: rate-limiting
config:
minute: 60
hour: 1000
policy: redis
redis_host: redis
redis_port: 6379
- name: cors
config:
origins:
- https://app.example.com
methods:
- GET
- POST
- PUT
- DELETE
headers:
- Authorization
- Content-Type
exposed_headers:
- X-RateLimit-Limit
- X-RateLimit-Remaining
credentials: true
max_age: 3600
- name: request-size-limiting
config:
allowed_payload_size: 10 # 10 MB
- name: response-transformer
config:
add:
headers:
- "X-API-Version: 2025-01-01"
- "Strict-Transport-Security: max-age=31536000"
AWS API Gateway with Lambda Authorizer:
// Lambda authorizer for JWT validation
export async function handler(event) {
const token = event.authorizationToken.replace('Bearer ', '');
try {
const payload = await validateJWT(token);
// Generate IAM policy
return {
principalId: payload.sub,
policyDocument: {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: 'Allow',
Resource: event.methodArn
}]
},
context: {
userId: payload.sub,
scopes: payload.scope,
tier: payload.subscription_tier
}
};
} catch (error) {
throw new Error('Unauthorized');
}
}
Gateway Security Patterns:
// Centralized authentication at gateway
async function gatewayAuthenticationMiddleware(request) {
// 1. Extract and validate JWT
const payload = await validateJWT(request);
// 2. Enrich request with user context
request.headers.set('X-User-ID', payload.sub);
request.headers.set('X-User-Scopes', payload.scope);
request.headers.set('X-User-Tier', payload.subscription_tier);
// 3. Remove sensitive headers before forwarding to backend
request.headers.delete('Authorization'); // Backend trusts gateway
return request;
}
// Request validation
async function validateRequest(request, schema) {
const contentType = request.headers.get('Content-Type');
// Validate Content-Type
if (!contentType || !contentType.includes('application/json')) {
throw new BadRequestError('Content-Type must be application/json');
}
// Validate payload size
const contentLength = parseInt(request.headers.get('Content-Length') || '0');
if (contentLength > 10 * 1024 * 1024) { // 10 MB
throw new PayloadTooLargeError('Request payload exceeds 10 MB limit');
}
// Validate against JSON schema
const body = await request.json();
const valid = await validateSchema(body, schema);
if (!valid.valid) {
throw new BadRequestError(`Validation failed: ${valid.errors.join(', ')}`);
}
return body;
}
Use the HTTP Request Builder to test your gateway configuration, verify authentication, check rate limiting headers, and debug routing issues.
Step 3.2: Security Headers Implementation (30-60 minutes)
HTTP security headers protect against common web vulnerabilities.
Complete Security Headers Configuration:
function addSecurityHeaders(response) {
// HSTS - Force HTTPS for 1 year
response.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// Prevent MIME type sniffing
response.setHeader('X-Content-Type-Options', 'nosniff');
// Prevent clickjacking
response.setHeader('X-Frame-Options', 'DENY');
// Referrer policy
response.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions policy
response.setHeader(
'Permissions-Policy',
'geolocation=(), camera=(), microphone=(), payment=()'
);
// Content Security Policy (for API documentation sites)
if (isDocumentationRoute(response.request.url)) {
response.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
}
// API-specific headers
response.setHeader('X-API-Version', '2025-01-01');
response.setHeader('X-Request-ID', response.requestId);
return response;
}
Audit your security headers using the Security Headers Analyzer to get a security grade and implementation recommendations.
Step 3.3: CORS Policy Configuration (45-90 minutes)
Cross-Origin Resource Sharing (CORS) controls which web applications can access your API from browsers.
CORS Implementation:
async function handleCORS(request, response) {
const origin = request.headers.get('Origin');
const method = request.method;
// Allowed origins (never use '*' with credentials)
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
'https://partner.example.com'
];
// Check if origin is allowed
if (origin && allowedOrigins.includes(origin)) {
response.setHeader('Access-Control-Allow-Origin', origin);
response.setHeader('Access-Control-Allow-Credentials', 'true');
}
// Handle preflight request (OPTIONS)
if (method === 'OPTIONS') {
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH');
response.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-Request-ID');
response.setHeader('Access-Control-Max-Age', '86400'); // 24 hours
response.setHeader('Access-Control-Expose-Headers', 'X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-ID');
return new Response(null, { status: 204, headers: response.headers });
}
// Expose headers for actual requests
if (origin && allowedOrigins.includes(origin)) {
response.setHeader('Access-Control-Expose-Headers', 'X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-ID');
}
return response;
}
CORS Security Validations:
function validateCORSConfiguration(config) {
const issues = [];
// Check for wildcard with credentials
if (config.allowOrigin === '*' && config.allowCredentials === true) {
issues.push({
severity: 'CRITICAL',
message: 'Wildcard origin (*) cannot be used with credentials',
fix: 'Specify exact allowed origins'
});
}
// Check for null origin
if (config.allowOrigin === 'null') {
issues.push({
severity: 'HIGH',
message: 'Allowing null origin is dangerous',
fix: 'Remove null from allowed origins'
});
}
// Check for origin reflection without validation
if (config.reflectOrigin && !config.originWhitelist) {
issues.push({
severity: 'CRITICAL',
message: 'Reflecting Origin header without validation allows any origin',
fix: 'Implement origin whitelist validation'
});
}
// Check for overly permissive methods
if (config.allowMethods.includes('*')) {
issues.push({
severity: 'MEDIUM',
message: 'Wildcard methods (*) are overly permissive',
fix: 'Specify exact allowed methods'
});
}
return issues;
}
Test your CORS configuration using the CORS Policy Analyzer to detect misconfigurations and validate policies.
Stage 4: Webhook Security Implementation (1.5-3 hours)
Webhooks enable real-time event notifications, but only 30% implement proper replay attack protection. Securing webhooks is critical for data integrity.
Step 4.1: Webhook Endpoint Design & Testing (30-60 minutes)
Webhook endpoints must handle events reliably while preventing abuse.
Webhook Endpoint Requirements:
// Production webhook endpoint
export async function POST(request) {
const requestId = crypto.randomUUID();
try {
// 1. Validate signature (prevent unauthorized events)
const signature = request.headers.get('X-Webhook-Signature');
const timestamp = request.headers.get('X-Webhook-Timestamp');
const rawBody = await request.text();
if (!await validateSignature(rawBody, signature, timestamp)) {
return new Response('Invalid signature', { status: 401 });
}
// 2. Parse event payload
const event = JSON.parse(rawBody);
// 3. Validate timestamp (prevent replay attacks)
const eventTime = parseInt(timestamp);
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime - eventTime > 300) { // 5 minutes
return new Response('Timestamp too old', { status: 400 });
}
// 4. Check idempotency (prevent duplicate processing)
const eventId = event.id;
const alreadyProcessed = await redis.exists(`webhook:processed:${eventId}`);
if (alreadyProcessed) {
// Return success without reprocessing
return new Response('Event already processed', { status: 200 });
}
// 5. Acknowledge receipt immediately (don't wait for processing)
queueEvent(event, requestId);
// 6. Mark as processed (24-hour TTL)
await redis.setex(`webhook:processed:${eventId}`, 86400, '1');
return new Response('Event received', { status: 200 });
} catch (error) {
// Log error but return appropriate status
console.error(`Webhook error [${requestId}]:`, error);
if (error instanceof ValidationError) {
return new Response(error.message, { status: 400 });
}
// Temporary error - provider will retry
return new Response('Internal error', { status: 500 });
}
}
// Asynchronous event processing
async function queueEvent(event, requestId) {
await queue.publish('webhook-events', {
event: event,
requestId: requestId,
receivedAt: new Date().toISOString()
});
}
Error Handling & Retry Semantics:
function getRetryableStatusCodes() {
return [
500, // Internal Server Error
502, // Bad Gateway
503, // Service Unavailable
504, // Gateway Timeout
429 // Too Many Requests
];
}
function getNonRetryableStatusCodes() {
return [
400, // Bad Request (invalid payload)
401, // Unauthorized (signature failure)
403, // Forbidden (access denied)
404, // Not Found (endpoint doesn't exist)
422 // Unprocessable Entity (business logic error)
];
}
Use the Webhook Tester & Inspector to generate test endpoints, capture payloads in real-time, and test your error handling.
Step 4.2: Signature Validation Implementation (45-90 minutes)
HMAC signature validation ensures webhooks originate from legitimate sources.
Stripe Webhook Signature Validation:
import crypto from 'crypto';
async function validateStripeSignature(rawBody, signatureHeader, secret) {
// Stripe signature format: t=timestamp,v1=signature
const elements = signatureHeader.split(',');
let timestamp, signature;
for (const element of elements) {
const [key, value] = element.split('=');
if (key === 't') timestamp = value;
if (key === 'v1') signature = value;
}
if (!timestamp || !signature) {
throw new Error('Invalid signature header format');
}
// Construct signed payload
const signedPayload = `${timestamp}.${rawBody}`;
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');
// Constant-time comparison (prevents timing attacks)
const isValid = crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
if (!isValid) {
throw new Error('Signature mismatch');
}
// Validate timestamp (5-minute tolerance)
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime - parseInt(timestamp) > 300) {
throw new Error('Timestamp too old - possible replay attack');
}
return true;
}
GitHub Webhook Signature Validation:
async function validateGitHubSignature(rawBody, signatureHeader, secret) {
// GitHub signature format: sha256=hexstring
if (!signatureHeader.startsWith('sha256=')) {
throw new Error('Invalid signature header format');
}
const receivedSignature = signatureHeader.substring(7);
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
// Constant-time comparison
const isValid = crypto.timingSafeEqual(
Buffer.from(receivedSignature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
if (!isValid) {
throw new Error('Signature mismatch');
}
return true;
}
Shopify Webhook Signature Validation:
async function validateShopifySignature(rawBody, signatureHeader, secret) {
// Shopify signature: Base64-encoded HMAC-SHA256
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('base64');
// Constant-time comparison
const isValid = crypto.timingSafeEqual(
Buffer.from(signatureHeader, 'base64'),
Buffer.from(expectedSignature, 'base64')
);
if (!isValid) {
throw new Error('Signature mismatch');
}
return true;
}
Universal Webhook Signature Validator:
const WEBHOOK_PROVIDERS = {
stripe: {
headerName: 'Stripe-Signature',
algorithm: 'sha256',
format: 'stripe-timestamp',
validate: validateStripeSignature
},
github: {
headerName: 'X-Hub-Signature-256',
algorithm: 'sha256',
format: 'prefixed-hex',
validate: validateGitHubSignature
},
shopify: {
headerName: 'X-Shopify-Hmac-Sha256',
algorithm: 'sha256',
format: 'base64',
validate: validateShopifySignature
},
slack: {
headerName: 'X-Slack-Signature',
algorithm: 'sha256',
format: 'slack-versioned',
validate: validateSlackSignature
}
};
async function validateWebhookSignature(provider, request, secret) {
const config = WEBHOOK_PROVIDERS[provider];
if (!config) {
throw new Error(`Unknown webhook provider: ${provider}`);
}
const signature = request.headers.get(config.headerName);
const rawBody = await request.text();
return await config.validate(rawBody, signature, secret);
}
Generate test webhook payloads with valid signatures using the Webhook Payload Generator.
Step 4.3: Replay Attack Prevention (30-60 minutes)
Replay attacks send the same valid webhook multiple times to cause unintended effects.
Timestamp Validation:
async function validateWebhookTimestamp(timestamp, maxAge = 300) {
const eventTime = parseInt(timestamp);
const currentTime = Math.floor(Date.now() / 1000);
// Check timestamp is not in the future (allow 30s clock skew)
if (eventTime > currentTime + 30) {
throw new Error('Timestamp is in the future');
}
// Check timestamp is not too old (default: 5 minutes)
if (currentTime - eventTime > maxAge) {
throw new Error('Timestamp too old - possible replay attack');
}
return true;
}
Idempotency Implementation:
// Redis-based idempotency
async function checkWebhookIdempotency(eventId, ttl = 86400) {
const key = `webhook:idempotency:${eventId}`;
// Check if already processed
const exists = await redis.exists(key);
if (exists) {
return { processed: true, duplicate: true };
}
// Mark as processing (prevents race conditions)
const acquired = await redis.set(key, 'processing', 'NX', 'EX', ttl);
if (!acquired) {
return { processed: true, duplicate: true };
}
return { processed: false, duplicate: false };
}
// After successful processing, mark as complete
async function markWebhookProcessed(eventId, result, ttl = 86400) {
const key = `webhook:idempotency:${eventId}`;
await redis.set(key, JSON.stringify({
status: 'completed',
result: result,
processedAt: new Date().toISOString()
}), 'EX', ttl);
}
Nonce Tracking (Advanced):
// For webhooks without built-in event IDs
async function generateAndValidateNonce(nonce) {
const key = `webhook:nonce:${nonce}`;
// Check if nonce already used
const exists = await redis.exists(key);
if (exists) {
throw new Error('Nonce already used - possible replay attack');
}
// Store nonce (24-hour expiration)
await redis.setex(key, 86400, '1');
return true;
}
Step 4.4: Retry Logic & Delivery Guarantees (30-60 minutes)
Webhooks should implement graceful retry with exponential backoff.
Webhook Delivery System (Provider Side):
class WebhookDeliveryService {
constructor() {
this.retrySchedule = [
60, // 1 minute
300, // 5 minutes
900, // 15 minutes
3600, // 1 hour
10800, // 3 hours
21600, // 6 hours
43200, // 12 hours
86400 // 24 hours
];
}
async deliverWebhook(event, endpoint, secret) {
let attempt = 0;
while (attempt < this.retrySchedule.length) {
try {
const response = await this.sendWebhook(event, endpoint, secret);
// Success - stop retrying
if (response.status >= 200 && response.status < 300) {
await this.recordDelivery(event.id, 'success', attempt + 1);
return { success: true, attempts: attempt + 1 };
}
// Client error - don't retry (except 429)
if (response.status >= 400 && response.status < 500) {
if (response.status !== 429) {
await this.recordDelivery(event.id, 'failed_permanent', attempt + 1);
return { success: false, reason: 'client_error', status: response.status };
}
}
// Server error or 429 - retry with backoff
attempt++;
if (attempt < this.retrySchedule.length) {
const delay = this.retrySchedule[attempt];
await this.scheduleRetry(event, endpoint, secret, delay, attempt);
}
} catch (error) {
// Network error - retry
attempt++;
if (attempt < this.retrySchedule.length) {
const delay = this.retrySchedule[attempt];
await this.scheduleRetry(event, endpoint, secret, delay, attempt);
}
}
}
// All retries exhausted
await this.recordDelivery(event.id, 'failed_permanent', attempt);
return { success: false, reason: 'max_retries_exceeded' };
}
async sendWebhook(event, endpoint, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const payload = JSON.stringify(event);
// Generate signature
const signature = this.generateSignature(payload, timestamp, secret);
return await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Timestamp': timestamp.toString(),
'X-Webhook-ID': event.id,
'User-Agent': 'WebhookService/1.0'
},
body: payload,
timeout: 30000 // 30 second timeout
});
}
generateSignature(payload, timestamp, secret) {
const signedPayload = `${timestamp}.${payload}`;
return crypto.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
}
async scheduleRetry(event, endpoint, secret, delaySeconds, attempt) {
await queue.publish('webhook-retries', {
event: event,
endpoint: endpoint,
secret: secret,
attempt: attempt,
scheduledFor: Date.now() + (delaySeconds * 1000)
}, { delay: delaySeconds * 1000 });
}
async recordDelivery(eventId, status, attempts) {
await db.webhookDeliveries.insert({
event_id: eventId,
status: status,
attempts: attempts,
completed_at: new Date()
});
}
}
Monitoring Webhook Health:
async function monitorWebhookHealth(endpoint) {
const last24h = Date.now() - (24 * 60 * 60 * 1000);
const stats = await db.webhookDeliveries.aggregate([
{ $match: { endpoint: endpoint, completed_at: { $gte: last24h } } },
{
$group: {
_id: '$status',
count: { $sum: 1 },
avgAttempts: { $avg: '$attempts' }
}
}
]);
const total = stats.reduce((sum, s) => sum + s.count, 0);
const successful = stats.find(s => s._id === 'success')?.count || 0;
const successRate = (successful / total) * 100;
// Alert if success rate < 90%
if (successRate < 90) {
await alertOps({
type: 'webhook_delivery_degradation',
endpoint: endpoint,
success_rate: successRate,
total_deliveries: total
});
}
return {
endpoint: endpoint,
total_deliveries: total,
success_rate: successRate,
average_attempts: stats.find(s => s._id === 'success')?.avgAttempts || 0,
stats: stats
};
}
Understand the correct retry semantics for different HTTP status codes using the HTTP Status Codes reference.
Stage 5: API Monetization & Quota Enforcement (2-3 hours)
The global API monetization market is growing at 11.9% CAGR, projected to reach $2.9B by 2035. Proper quota management enables sustainable API businesses.
Step 5.1: Tiered Pricing Structure Design (45-90 minutes)
Effective tiered pricing follows the 2-3x rule: each tier costs 2-3x the previous tier while providing 5-10x the value.
Example Tiered Pricing Matrix:
const API_PRICING_TIERS = {
free: {
price: 0,
billing_period: 'month',
quota: {
requests_per_month: 10000,
requests_per_minute: 10,
burst_capacity: 20
},
features: {
basic_endpoints: true,
advanced_endpoints: false,
webhooks: false,
batch_operations: false,
priority_support: false,
sla: null,
custom_integrations: false
},
limits: {
max_payload_size_mb: 1,
max_response_size_mb: 1,
data_retention_days: 7
}
},
builder: {
price: 49,
billing_period: 'month',
quota: {
requests_per_month: 100000,
requests_per_minute: 60,
burst_capacity: 100
},
features: {
basic_endpoints: true,
advanced_endpoints: true,
webhooks: true,
batch_operations: false,
priority_support: false,
sla: '99.0%',
custom_integrations: false
},
limits: {
max_payload_size_mb: 5,
max_response_size_mb: 5,
data_retention_days: 30
},
overage: {
strategy: 'soft_quota',
throttled_rate: 10
}
},
pro: {
price: 199,
billing_period: 'month',
quota: {
requests_per_month: 1000000,
requests_per_minute: 600,
burst_capacity: 1000
},
features: {
basic_endpoints: true,
advanced_endpoints: true,
webhooks: true,
batch_operations: true,
priority_support: true,
sla: '99.9%',
custom_integrations: true
},
limits: {
max_payload_size_mb: 25,
max_response_size_mb: 25,
data_retention_days: 90
},
overage: {
strategy: 'billing',
price_per_request: 0.0002 // $0.0002 per request
}
},
enterprise: {
price: 'custom',
billing_period: 'custom',
quota: {
requests_per_month: null, // Unlimited
requests_per_minute: 'custom',
burst_capacity: 'custom'
},
features: {
basic_endpoints: true,
advanced_endpoints: true,
webhooks: true,
batch_operations: true,
priority_support: true,
sla: '99.99%',
custom_integrations: true,
dedicated_infrastructure: true,
on_premise_deployment: true,
white_label: true
},
limits: {
max_payload_size_mb: 'custom',
max_response_size_mb: 'custom',
data_retention_days: 'custom'
}
}
};
Pricing Psychology Principles:
- Anchor with highest tier first in marketing materials
- Highlight "most popular" tier (usually second-highest)
- Offer annual discount (typically 20% off monthly price)
- Include feature comparison table showing value progression
- Add usage calculator so customers can estimate costs
Step 5.2: Usage Tracking & Metering (45-90 minutes)
Real-time usage tracking enables accurate billing and proactive customer communication.
Comprehensive Usage Tracking:
class UsageTrackingService {
async trackRequest(apiKey, endpoint, method, responseTime, statusCode) {
const now = Date.now();
const month = new Date().toISOString().slice(0, 7);
const day = new Date().toISOString().slice(0, 10);
const hour = new Date().toISOString().slice(0, 13);
// Increment monthly counter (for quota enforcement)
const monthlyKey = `usage:monthly:${apiKey}:${month}`;
await redis.incr(monthlyKey);
await redis.expire(monthlyKey, 90 * 86400); // 90 days retention
// Track endpoint-specific usage
const endpointKey = `usage:endpoint:${apiKey}:${month}`;
await redis.hincrby(endpointKey, `${method}:${endpoint}`, 1);
await redis.expire(endpointKey, 90 * 86400);
// Track hourly usage (for charts and analytics)
const hourlyKey = `usage:hourly:${apiKey}:${day}`;
await redis.hincrby(hourlyKey, hour, 1);
await redis.expire(hourlyKey, 30 * 86400);
// Track response times (for performance monitoring)
await redis.lpush(`usage:response_times:${apiKey}`, responseTime);
await redis.ltrim(`usage:response_times:${apiKey}`, 0, 999); // Keep last 1000
// Track status codes (for error monitoring)
const statusKey = `usage:status:${apiKey}:${day}`;
await redis.hincrby(statusKey, statusCode.toString(), 1);
await redis.expire(statusKey, 30 * 86400);
// Store detailed event (for billing and debugging)
await db.usageEvents.insert({
api_key: apiKey,
endpoint: endpoint,
method: method,
status_code: statusCode,
response_time_ms: responseTime,
timestamp: new Date(now),
billing_month: month
});
}
async getUsageSummary(apiKey) {
const month = new Date().toISOString().slice(0, 7);
// Get monthly usage
const monthlyUsage = parseInt(await redis.get(`usage:monthly:${apiKey}:${month}`)) || 0;
// Get quota limit
const tier = await this.getTier(apiKey);
const quota = API_PRICING_TIERS[tier].quota.requests_per_month;
// Get endpoint breakdown
const endpointUsage = await redis.hgetall(`usage:endpoint:${apiKey}:${month}`);
// Calculate forecast
const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
const dayOfMonth = new Date().getDate();
const projectedUsage = Math.ceil((monthlyUsage / dayOfMonth) * daysInMonth);
// Calculate days until quota exhausted (if current rate continues)
const dailyRate = monthlyUsage / dayOfMonth;
const remainingQuota = quota - monthlyUsage;
const daysUntilExhausted = dailyRate > 0 ? Math.floor(remainingQuota / dailyRate) : Infinity;
return {
current_usage: monthlyUsage,
quota_limit: quota,
percentage_used: (monthlyUsage / quota) * 100,
remaining: quota - monthlyUsage,
projected_end_of_month: projectedUsage,
days_until_exhausted: daysUntilExhausted,
endpoint_breakdown: endpointUsage,
reset_at: this.getNextBillingCycle()
};
}
async sendQuotaAlerts(apiKey, currentUsage, quota) {
const percentage = (currentUsage / quota) * 100;
const alertKey = `usage:alert_sent:${apiKey}`;
// Check if we've already sent this threshold alert
const lastAlert = await redis.get(alertKey);
if (percentage >= 100 && lastAlert !== '100') {
await this.sendAlert(apiKey, '100', currentUsage, quota);
await redis.set(alertKey, '100');
} else if (percentage >= 90 && lastAlert !== '90' && lastAlert !== '100') {
await this.sendAlert(apiKey, '90', currentUsage, quota);
await redis.set(alertKey, '90');
} else if (percentage >= 80 && !lastAlert) {
await this.sendAlert(apiKey, '80', currentUsage, quota);
await redis.set(alertKey, '80');
}
}
async sendAlert(apiKey, threshold, currentUsage, quota) {
const customer = await db.customers.findOne({ api_key: apiKey });
const webhookPayload = {
event: threshold === '100' ? 'quota.exhausted' : 'quota.warning',
api_key: apiKey,
customer_id: customer.id,
threshold_percentage: parseInt(threshold),
current_usage: currentUsage,
quota_limit: quota,
period_start: this.getCurrentBillingCycleStart().toISOString(),
period_end: this.getNextBillingCycle().toISOString()
};
// Send webhook
if (customer.webhook_url) {
await sendWebhook(customer.webhook_url, webhookPayload, customer.webhook_secret);
}
// Send email
await sendEmail(customer.email, `quota-alert-${threshold}`, webhookPayload);
}
}
Usage Analytics Dashboard Data:
async function getUsageAnalytics(apiKey, startDate, endDate) {
const events = await db.usageEvents.find({
api_key: apiKey,
timestamp: { $gte: startDate, $lte: endDate }
});
// Daily usage trend
const dailyUsage = {};
events.forEach(event => {
const day = event.timestamp.toISOString().slice(0, 10);
dailyUsage[day] = (dailyUsage[day] || 0) + 1;
});
// Endpoint popularity
const endpointStats = {};
events.forEach(event => {
const key = `${event.method} ${event.endpoint}`;
if (!endpointStats[key]) {
endpointStats[key] = { count: 0, totalResponseTime: 0 };
}
endpointStats[key].count++;
endpointStats[key].totalResponseTime += event.response_time_ms;
});
// Status code distribution
const statusCodes = {};
events.forEach(event => {
statusCodes[event.status_code] = (statusCodes[event.status_code] || 0) + 1;
});
// Performance percentiles
const responseTimes = events.map(e => e.response_time_ms).sort((a, b) => a - b);
const p50 = responseTimes[Math.floor(responseTimes.length * 0.5)];
const p95 = responseTimes[Math.floor(responseTimes.length * 0.95)];
const p99 = responseTimes[Math.floor(responseTimes.length * 0.99)];
return {
total_requests: events.length,
daily_usage: dailyUsage,
endpoint_stats: endpointStats,
status_codes: statusCodes,
performance: {
p50_ms: p50,
p95_ms: p95,
p99_ms: p99,
avg_ms: responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
}
};
}
Step 5.3: Overage Handling & Upgrade Flows (45-90 minutes)
Different overage strategies suit different business models.
Overage Enforcement:
async function handleOverage(apiKey, currentUsage, quota, tier) {
const overageStrategy = API_PRICING_TIERS[tier].overage;
if (!overageStrategy) {
// Hard stop (default for free tier)
return {
allowed: false,
response: {
status: 402,
body: {
error: 'quota_exceeded',
message: `Your ${tier} plan quota of ${quota.toLocaleString()} requests per month has been exhausted`,
current_usage: currentUsage,
quota: quota,
reset_at: getNextBillingCycle().toISOString(),
upgrade_url: `https://api.example.com/billing/upgrade?key=${apiKey}`
}
}
};
}
if (overageStrategy.strategy === 'soft_quota') {
// Throttle rate limit but allow continued access
return {
allowed: true,
throttled: true,
new_rate_limit: overageStrategy.throttled_rate,
warning: `Quota exceeded - throttled to ${overageStrategy.throttled_rate} requests/minute until ${getNextBillingCycle().toISOString()}`
};
}
if (overageStrategy.strategy === 'billing') {
// Charge for overage
const overageRequests = currentUsage - quota;
const overageCharge = overageRequests * overageStrategy.price_per_request;
// Record overage charge
await db.overageCharges.insert({
api_key: apiKey,
billing_month: new Date().toISOString().slice(0, 7),
overage_requests: overageRequests,
charge_amount: overageCharge,
recorded_at: new Date()
});
return {
allowed: true,
overage: true,
overage_requests: overageRequests,
overage_charge: overageCharge,
warning: `${overageRequests.toLocaleString()} requests over quota ($${overageCharge.toFixed(2)} overage charge)`
};
}
}
Self-Service Upgrade Flow:
// API endpoint for plan upgrades
app.post('/api/billing/upgrade', validateJWT, async (req, res) => {
const { new_tier } = req.body;
const apiKey = req.payload.api_key;
// Validate tier
if (!API_PRICING_TIERS[new_tier]) {
return res.status(400).json({ error: 'Invalid tier' });
}
// Get current tier
const currentTier = await getTier(apiKey);
const currentPrice = API_PRICING_TIERS[currentTier].price;
const newPrice = API_PRICING_TIERS[new_tier].price;
// Prevent downgrades mid-cycle (or calculate prorated refund)
if (newPrice < currentPrice) {
return res.status(400).json({
error: 'downgrades_not_allowed',
message: 'Downgrades take effect at next billing cycle'
});
}
// Calculate prorated charge
const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
const dayOfMonth = new Date().getDate();
const remainingDays = daysInMonth - dayOfMonth;
const proratedCharge = ((newPrice - currentPrice) / daysInMonth) * remainingDays;
// Create Stripe payment intent
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(proratedCharge * 100), // Cents
currency: 'usd',
metadata: {
api_key: apiKey,
upgrade_from: currentTier,
upgrade_to: new_tier,
type: 'prorated_upgrade'
}
});
return res.json({
client_secret: paymentIntent.client_secret,
prorated_charge: proratedCharge,
new_tier: new_tier,
effective_immediately: true,
next_full_charge: newPrice,
next_billing_date: getNextBillingCycle().toISOString()
});
});
// Webhook handler for successful upgrade payment
app.post('/webhooks/stripe', async (req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(req.rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET);
if (event.type === 'payment_intent.succeeded') {
const paymentIntent = event.data.object;
if (paymentIntent.metadata.type === 'prorated_upgrade') {
// Upgrade tier immediately
await db.customers.update(
{ api_key: paymentIntent.metadata.api_key },
{ $set: { tier: paymentIntent.metadata.upgrade_to } }
);
// Send confirmation email
await sendUpgradeConfirmation(paymentIntent.metadata.api_key, paymentIntent.metadata.upgrade_to);
// Clear rate limit cache to apply new limits immediately
await redis.del(`ratelimit:${paymentIntent.metadata.api_key}`);
}
}
res.json({ received: true });
});
Use the Rate Limit Calculator to model tiered pricing, calculate optimal quotas, and design fair overage policies.
Stage 6: Monitoring, Logging & Incident Response (2-3 hours)
Comprehensive observability enables rapid threat detection and incident response, reducing mean time to resolution (MTTR) from hours to minutes.
Step 6.1: API Security Logging (45-90 minutes)
Structured logging provides the foundation for security monitoring and compliance.
Comprehensive Request Logging:
class APILogger {
async logRequest(request, response, startTime) {
const duration = Date.now() - startTime;
const logEntry = {
// Request metadata
timestamp: new Date().toISOString(),
request_id: request.id,
level: this.getLogLevel(response.statusCode),
// HTTP details
method: request.method,
path: request.path,
status_code: response.statusCode,
response_time_ms: duration,
// User context
user: {
id: request.user?.id,
email: request.user?.email,
ip_address: request.ip,
user_agent: request.headers['user-agent'],
country: request.geo?.country,
city: request.geo?.city
},
// Authentication
auth: {
method: request.authMethod, // jwt, api_key, etc.
token_id: request.tokenId,
scopes: request.scopes,
api_key: this.maskApiKey(request.apiKey)
},
// Quota and rate limiting
quota: {
current_usage: request.quotaUsage,
quota_limit: request.quotaLimit,
percentage_used: (request.quotaUsage / request.quotaLimit) * 100
},
rate_limit: {
limit: request.rateLimit,
remaining: request.rateLimitRemaining,
reset_at: request.rateLimitReset
},
// Request/response size
request_size_bytes: request.contentLength,
response_size_bytes: response.contentLength,
// Error details (if applicable)
error: response.statusCode >= 400 ? {
code: response.errorCode,
message: response.errorMessage,
stack: process.env.NODE_ENV === 'development' ? response.errorStack : undefined
} : undefined
};
// Send to logging service
await this.sendLog(logEntry);
// Send to SIEM for security events
if (this.isSecurityEvent(logEntry)) {
await this.sendToSIEM(logEntry);
}
}
getLogLevel(statusCode) {
if (statusCode >= 500) return 'ERROR';
if (statusCode >= 400) return 'WARN';
return 'INFO';
}
maskApiKey(apiKey) {
if (!apiKey) return null;
const prefix = apiKey.substring(0, 8);
return `${prefix}_[REDACTED]`;
}
isSecurityEvent(logEntry) {
return (
logEntry.status_code === 401 || // Authentication failure
logEntry.status_code === 403 || // Authorization failure
logEntry.status_code === 429 || // Rate limit exceeded
logEntry.error?.code === 'invalid_signature' ||
logEntry.error?.code === 'token_expired' ||
logEntry.error?.code === 'quota_exceeded'
);
}
async sendToSIEM(logEntry) {
// Enrich with threat intelligence
const ipRisk = await checkIPRisk(logEntry.user.ip_address);
const siemEvent = {
...logEntry,
event_type: this.categorizeSecurityEvent(logEntry),
threat_intelligence: {
ip_reputation_score: ipRisk.reputation_score,
vpn_detected: ipRisk.vpn_detected,
tor_detected: ipRisk.tor_detected,
blocklist_matches: ipRisk.blocklist_matches,
country_risk_level: ipRisk.country_risk_level
}
};
await sendToSplunk(siemEvent);
// or await sendToDatadog(siemEvent);
// or await sendToElasticsearch(siemEvent);
}
categorizeSecurityEvent(logEntry) {
if (logEntry.status_code === 401) return 'authentication_failure';
if (logEntry.status_code === 403) return 'authorization_failure';
if (logEntry.status_code === 429) return 'rate_limit_exceeded';
if (logEntry.error?.code === 'invalid_signature') return 'signature_validation_failure';
return 'security_event';
}
}
PII Redaction:
function redactPII(logEntry) {
// Redact email addresses
if (logEntry.user?.email) {
logEntry.user.email = redactEmail(logEntry.user.email);
}
// Redact credit card numbers
if (logEntry.request_body) {
logEntry.request_body = logEntry.request_body.replace(
/\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}/g,
'[REDACTED_CC]'
);
}
// Redact SSNs
if (logEntry.request_body) {
logEntry.request_body = logEntry.request_body.replace(
/\\d{3}-\\d{2}-\\d{4}/g,
'[REDACTED_SSN]'
);
}
return logEntry;
}
function redactEmail(email) {
const [local, domain] = email.split('@');
const maskedLocal = local.charAt(0) + '*'.repeat(local.length - 1);
return `${maskedLocal}@${domain}`;
}
Step 6.2: Rate Limit & Quota Monitoring (30-60 minutes)
Proactive monitoring prevents quota exhaustion surprises and identifies optimization opportunities.
Rate Limit Metrics Dashboard:
async function getRateLimitMetrics(timeRange = '24h') {
const startTime = new Date(Date.now() - parseTimeRange(timeRange));
const metrics = await db.usageEvents.aggregate([
{ $match: { timestamp: { $gte: startTime } } },
{
$group: {
_id: {
api_key: '$api_key',
hour: { $dateToString: { format: '%Y-%m-%d %H:00', date: '$timestamp' } }
},
total_requests: { $sum: 1 },
rate_limited: {
$sum: { $cond: [{ $eq: ['$status_code', 429] }, 1, 0] }
},
avg_response_time: { $avg: '$response_time_ms' },
p95_response_time: { $percentile: { input: '$response_time_ms', p: [0.95], method: 'approximate' } }
}
}
]);
// Calculate rejection rates
const analyticsData = metrics.map(m => ({
api_key: m._id.api_key,
hour: m._id.hour,
total_requests: m.total_requests,
rate_limited: m.rate_limited,
rejection_rate: (m.rate_limited / m.total_requests) * 100,
avg_response_time: m.avg_response_time,
p95_response_time: m.p95_response_time[0]
}));
// Identify problematic clients (>10% rejection rate)
const problematicClients = analyticsData
.filter(a => a.rejection_rate > 10)
.map(a => a.api_key)
.filter((v, i, a) => a.indexOf(v) === i);
return {
metrics: analyticsData,
problematic_clients: problematicClients,
summary: {
total_requests: analyticsData.reduce((sum, a) => sum + a.total_requests, 0),
total_rate_limited: analyticsData.reduce((sum, a) => sum + a.rate_limited, 0),
overall_rejection_rate: (analyticsData.reduce((sum, a) => sum + a.rate_limited, 0) / analyticsData.reduce((sum, a) => sum + a.total_requests, 0)) * 100
}
};
}
Alerting Rules:
const ALERT_RULES = {
high_rejection_rate: {
condition: (metrics) => metrics.rejection_rate > 10,
severity: 'warning',
message: (metrics) => `High rate limit rejection rate: ${metrics.rejection_rate.toFixed(2)}% for ${metrics.api_key}`,
action: 'investigate_client_integration'
},
quota_threshold_80: {
condition: (usage) => (usage.current_usage / usage.quota_limit) >= 0.8,
severity: 'info',
message: (usage) => `80% quota threshold reached for ${usage.api_key}`,
action: 'send_upgrade_prompt'
},
quota_threshold_90: {
condition: (usage) => (usage.current_usage / usage.quota_limit) >= 0.9,
severity: 'warning',
message: (usage) => `90% quota threshold reached for ${usage.api_key}`,
action: 'urgent_upgrade_prompt'
},
quota_exhausted: {
condition: (usage) => usage.current_usage >= usage.quota_limit,
severity: 'critical',
message: (usage) => `Quota exhausted for ${usage.api_key}`,
action: 'send_upgrade_required'
},
unusual_spike: {
condition: (current, baseline) => current > baseline * 3,
severity: 'warning',
message: (current, baseline) => `Unusual traffic spike: ${current} requests (baseline: ${baseline})`,
action: 'investigate_potential_abuse'
}
};
Step 6.3: Security Incident Detection (45-90 minutes)
Automated threat detection identifies attacks in real-time.
Brute Force Detection:
async function detectBruteForce(ipAddress, endpoint) {
const window = 3600; // 1 hour
const threshold = 10;
const key = `security:brute_force:${ipAddress}:${endpoint}`;
const attempts = await redis.incr(key);
if (attempts === 1) {
await redis.expire(key, window);
}
if (attempts >= threshold) {
await triggerSecurityAlert({
type: 'brute_force_attack',
ip_address: ipAddress,
endpoint: endpoint,
failed_attempts: attempts,
time_window: '1 hour',
recommended_action: 'block_ip'
});
// Temporary IP block
await redis.setex(`security:blocked_ip:${ipAddress}`, 3600, '1');
return { blocked: true, reason: 'brute_force_detected' };
}
return { blocked: false };
}
Credential Stuffing Detection:
async function detectCredentialStuffing(ipAddress, username) {
const window = 3600;
const threshold = 5; // More than 5 different accounts from same IP
const key = `security:credential_stuffing:${ipAddress}`;
const accounts = await redis.sadd(key, username);
if (accounts === 1) {
await redis.expire(key, window);
}
const accountCount = await redis.scard(key);
if (accountCount >= threshold) {
await triggerSecurityAlert({
type: 'credential_stuffing',
ip_address: ipAddress,
accounts_targeted: accountCount,
time_window: '1 hour'
});
return { suspicious: true, block: true };
}
return { suspicious: accountCount >= 3 };
}
API Key Leakage Detection:
async function detectAPIKeyAnomaly(apiKey, ipAddress) {
// Get historical IP locations for this API key
const historicalIPs = await db.apiKeyUsage.distinct('ip_address', {
api_key: apiKey,
timestamp: { $gte: new Date(Date.now() - 30 * 86400000) } // Last 30 days
});
// Get geolocation
const currentGeo = await getIPGeolocation(ipAddress);
// Check if this IP is new
if (!historicalIPs.includes(ipAddress)) {
const historicalCountries = await Promise.all(
historicalIPs.slice(0, 10).map(ip => getIPGeolocation(ip).then(g => g.country))
);
const uniqueCountries = [...new Set(historicalCountries)];
// Alert if API key used from new country
if (!uniqueCountries.includes(currentGeo.country)) {
await triggerSecurityAlert({
type: 'api_key_anomaly',
api_key: apiKey,
previous_countries: uniqueCountries,
current_country: currentGeo.country,
current_ip: ipAddress,
recommended_action: 'verify_with_customer'
});
// Require additional verification
return { require_verification: true };
}
}
return { require_verification: false };
}
Enrich security logs with threat intelligence using the IP Risk Checker for geolocation, reputation scoring, and VPN detection.
Step 6.4: Incident Response Playbooks (45-90 minutes)
Predefined playbooks accelerate incident response and ensure consistent handling.
API Key Compromise Playbook:
class IncidentResponse {
async handleCompromisedAPIKey(apiKey, detectionReason) {
const incident = await this.createIncident({
type: 'api_key_compromise',
severity: 'high',
api_key: apiKey,
detection_reason: detectionReason,
status: 'detected'
});
// Step 1: Immediate response (within 5 minutes)
await this.revokeAPIKey(apiKey);
await this.blockSuspiciousIPs(apiKey);
await this.notifyCustomer(apiKey, incident.id);
await this.updateIncident(incident.id, { status: 'contained' });
// Step 2: Investigation (within 30 minutes)
const auditTrail = await this.getAuditTrail(apiKey);
const dataAccessed = await this.identifyDataAccessed(apiKey, auditTrail);
const scope = await this.determineBreach Scope(apiKey);
await this.updateIncident(incident.id, {
status: 'investigating',
audit_trail: auditTrail,
data_accessed: dataAccessed,
scope: scope
});
// Step 3: Remediation (within 2 hours)
const newAPIKey = await this.issueNewAPIKey(apiKey);
await this.rotateWebhookSecrets(apiKey);
await this.updateSecurityRules(detectionReason);
await this.updateIncident(incident.id, {
status: 'remediated',
new_api_key: newAPIKey
});
// Step 4: Post-incident (within 24 hours)
await this.documentIncident(incident.id);
await this.notifyAffectedParties(dataAccessed);
await this.conductLessonsLearned(incident.id);
await this.updateIncident(incident.id, { status: 'closed' });
}
async revokeAPIKey(apiKey) {
await db.apiKeys.update(
{ key: apiKey },
{ $set: { revoked: true, revoked_at: new Date(), revoked_reason: 'security_incident' } }
);
// Clear from cache
await redis.del(`apikey:${apiKey}`);
}
async notifyCustomer(apiKey, incidentId) {
const customer = await db.customers.findOne({ api_key: apiKey });
const notification = {
event: 'security.incident',
incident_id: incidentId,
incident_type: 'api_key_compromise',
severity: 'high',
api_key: this.maskAPIKey(apiKey),
recommended_action: 'Your API key has been automatically revoked. Please check your email for a new API key.',
timestamp: new Date().toISOString()
};
// Send webhook
if (customer.webhook_url) {
await sendWebhook(customer.webhook_url, notification, customer.webhook_secret);
}
// Send email
await sendEmail(customer.email, 'security-incident', notification);
}
}
DDoS Attack Playbook:
async function handleDDoSAttack() {
const incident = await createIncident({
type: 'ddos_attack',
severity: 'critical'
});
// Immediate response (within 2 minutes)
await enableGlobalRateLimiting();
await activateDDoSProtection();
const attackSources = await identifyAttackSources();
// Mitigation (within 10 minutes)
await blockAttackIPs(attackSources);
await enableCDNCaching();
await scaleBackendInfrastructure();
// Recovery (within 1 hour)
await verifyServiceRestoration();
await monitorForContinuation();
await adjustRateLimits(attackSources);
// Post-incident
await analyzeAttackVector(attackSources);
await updateDDoSProtectionRules();
await documentIncident(incident.id);
}
This comprehensive monitoring and incident response infrastructure ensures rapid detection and resolution of security threats, maintaining API availability and customer trust.
Frequently Asked Questions (FAQ)
1. What is the difference between OAuth 2.0 and OAuth 2.1?
OAuth 2.1 is a consolidation of OAuth 2.0 with several security best practices incorporated as requirements rather than recommendations. Key differences include:
- PKCE is mandatory for all client types (previously optional for confidential clients)
- Implicit grant and ROPC flows removed due to security vulnerabilities
- Exact redirect URI matching required (no wildcards or pattern matching allowed)
- Bearer tokens forbidden in URL query parameters (must use Authorization header)
- Refresh token rotation recommended to limit damage from token leakage
OAuth 2.1 represents current best practices as of 2025 and should be the target for all new implementations.
2. How do I choose between RS256 and HS256 for JWT signing?
Use RS256 (RSA) when:
- Multiple services need to validate tokens (public key can be safely distributed)
- You have a distributed microservices architecture
- Tokens are validated by third parties
- You need key rotation without redistributing secrets
Use HS256 (HMAC) when:
- Single service validates tokens (no key distribution needed)
- Performance is critical (HS256 is faster than RS256)
- You have a monolithic architecture
- Key management infrastructure is limited
RS256 is recommended for most modern APIs due to its superior security properties and support for distributed validation.
3. What rate limiting algorithm should I use for my API?
Token Bucket for most REST APIs - allows bursts while enforcing long-term limits, good for variable traffic patterns.
Sliding Window for high-value operations (financial transactions, LLM API calls) - fairest distribution, prevents burst exploits.
Leaky Bucket for backend write operations (database writes, email sending) - smooths traffic, protects backend resources.
Fixed Window for internal/low-security APIs - simplest implementation, lowest overhead, acceptable burst vulnerability.
Consider your traffic patterns, backend capacity, and fairness requirements when selecting an algorithm.
4. How can I prevent replay attacks on my webhook endpoints?
Implement three layers of protection:
Timestamp validation: Reject events older than 5 minutes (included in signature to prevent tampering).
Idempotency: Track processed event IDs in Redis with 24-hour TTL, return success for duplicates without reprocessing.
Signature validation: Verify HMAC-SHA256 signature using constant-time comparison to prevent timing attacks.
This combination prevents replay attacks while handling legitimate retries gracefully.
5. What HTTP status code should I return when rate limits are exceeded?
Return 429 Too Many Requests with these headers:
Retry-After: Seconds until rate limit resetsX-RateLimit-Limit: Total requests allowed per windowX-RateLimit-Remaining: Requests remaining (0 in this case)X-RateLimit-Reset: Unix timestamp when limit resets
Include a JSON response body with error details and reset time. This allows clients to implement proper retry logic with exponential backoff.
6. Should I use different rate limits for different API endpoints?
Yes. Different endpoints have different resource costs and security requirements:
- Authentication endpoints: Very restrictive (5 requests/minute) to prevent brute force
- Expensive operations: Moderate limits (10-20 requests/minute) for searches, reports
- Read operations: Generous limits (100+ requests/minute) for GET requests
- Write operations: Moderate limits (20-50 requests/minute) for POST/PUT/DELETE
- Sensitive operations: Very restrictive (1-5 requests/minute) for deletions, refunds
Configure endpoint-specific overrides on top of tier-based defaults.
7. How do I handle API quota overages for paid customers?
Three common strategies:
Hard stop (Free tier): Block all requests, return 402 Payment Required with upgrade URL.
Soft quota (Builder tier): Allow overage but throttle rate limit to 10-20% of normal, add warning headers.
Overage billing (Pro tier): Charge per additional request (e.g., $0.0001-$0.001 per request), record for billing.
Enterprise tiers typically have unlimited quotas or custom negotiated limits. Send proactive alerts at 80%, 90%, and 100% thresholds regardless of strategy.
8. What security headers are essential for APIs?
Critical headers:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload(force HTTPS)X-Content-Type-Options: nosniff(prevent MIME sniffing)Referrer-Policy: strict-origin-when-cross-origin(control referrer leakage)
API-specific headers:
X-API-Version: 2025-01-01(version tracking)X-Request-ID: req_abc123(tracing/debugging)X-Response-Time: 45ms(performance monitoring)
For API documentation sites, also include Content-Security-Policy and X-Frame-Options.
9. How do I configure CORS securely for my API?
Never use wildcard origin with credentials:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
This combination is rejected by browsers.
Use whitelist validation:
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
const origin = request.headers.get('Origin');
if (allowedOrigins.includes(origin)) {
response.setHeader('Access-Control-Allow-Origin', origin);
response.setHeader('Access-Control-Allow-Credentials', 'true');
}
Limit allowed methods, headers, and set appropriate preflight cache (24 hours recommended).
10. What is object-level authorization and why is it critical?
Object-level authorization verifies the user has permission to access a SPECIFIC resource, not just the resource type. This prevents OWASP API1 (Broken Object Level Authorization).
Vulnerable code:
app.get('/api/orders/:orderId', requireScopes('read:orders'), async (req, res) => {
const order = await db.orders.findById(req.params.orderId);
res.json(order); // Security issue: no ownership check
});
Secure code:
app.get('/api/orders/:orderId', requireScopes('read:orders'), async (req, res) => {
const order = await db.orders.findById(req.params.orderId);
const userId = req.user.id;
const isAdmin = req.scopes.includes('admin:orders');
if (order.user_id !== userId && !isAdmin) {
throw new NotFoundError(); // Return 404 to prevent info leakage
}
res.json(order);
});
Always verify ownership or admin privileges before returning resource data.
11. How should I store API keys and secrets?
Server-side (API provider):
- Hash API keys using bcrypt or Argon2 before storing in database
- Store webhook secrets encrypted using AES-256
- Use environment variables or secret managers (AWS Secrets Manager, HashiCorp Vault)
- Never commit secrets to git repositories
Client-side (API consumer):
- Web apps: Server-side only, never expose to browser
- Mobile apps: Secure platform storage (iOS Keychain, Android KeyStore)
- Single Page Apps: Use Backend-for-Frontend (BFF) pattern, proxy API calls through your backend
- CLI tools: Encrypted configuration files with OS keychain integration
Rotate API keys regularly (every 90 days recommended) and immediately upon suspected compromise.
12. What logging is required for security compliance (SOC 2, HIPAA)?
Compliance frameworks require comprehensive audit trails:
Authentication events:
- All login attempts (successful and failed)
- Token issuance and revocation
- Password changes and resets
- MFA challenges
Authorization events:
- Access to sensitive resources
- Permission changes
- Role assignments
- Failed authorization attempts
Data access:
- Who accessed what data, when
- Data modifications (create, update, delete)
- Data exports and downloads
- API calls to sensitive endpoints
System events:
- Configuration changes
- Security policy updates
- Rate limit violations
- Unusual traffic patterns
Retain logs for minimum 1 year (7 years for HIPAA). Implement log integrity verification and encrypt logs at rest.
13. How do I implement API versioning?
Three common approaches:
URL versioning (recommended for most APIs):
GET https://api.example.com/v1/orders
GET https://api.example.com/v2/orders
Header versioning:
GET https://api.example.com/orders
X-API-Version: 2025-01-01
Content negotiation:
GET https://api.example.com/orders
Accept: application/vnd.example.v2+json
Best practices:
- Support minimum 2 versions simultaneously
- Announce deprecation 6-12 months in advance
- Include Sunset header:
Sunset: Sat, 31 Dec 2025 23:59:59 GMT - Return 410 Gone for retired versions
- Document migration guides for breaking changes
14. How do I detect and prevent API abuse?
Implement multiple detection layers:
Rate limiting: Per-client quotas prevent resource exhaustion
Anomaly detection:
- Traffic spikes (>3x baseline)
- Geographic anomalies (new countries)
- User agent changes
- Request pattern changes
Behavioral analysis:
- Excessive error rates (>25% indicates misconfiguration or attack)
- Unusual endpoint access patterns
- Data scraping patterns (sequential ID enumeration)
Threat intelligence:
- IP reputation checking
- VPN/proxy detection
- Blocklist integration (Spamhaus, AbuseIPDB)
Automated responses: temporary IP blocks, CAPTCHA challenges, account verification requirements.
15. What metrics should I monitor for API health?
Performance metrics:
- Response time percentiles (p50, p95, p99)
- Request throughput (requests/second)
- Error rates by status code (4xx, 5xx)
- Availability/uptime percentage
Security metrics:
- Authentication failure rate
- Authorization failure rate
- Rate limit hit rate
- Quota exhaustion frequency
- Security incident count
Business metrics:
- API calls per customer
- Feature adoption rates
- Quota utilization trends
- Upgrade conversion rates
Set alerts for:
- p95 latency > 500ms
- 5xx error rate > 1%
- Authentication failure rate > 5%
- Rate limit rejection rate > 10%
Conclusion
This comprehensive 6-stage workflow provides production-grade API security covering authentication, authorization, rate limiting, webhook security, monetization, and monitoring. Implementing these patterns addresses the OWASP API Security Top 10, satisfies compliance requirements (SOC 2, HIPAA, PSD2), and enables sustainable API-first business models.
Key Achievements:
✓ OAuth 2.1 compliance with PKCE for all client types ✓ FAPI-grade security with comprehensive JWT validation ✓ Advanced rate limiting using token bucket and sliding window algorithms ✓ Webhook security with HMAC signature validation and replay protection ✓ API monetization with tiered pricing and usage-based billing ✓ Comprehensive observability with structured logging and threat detection
Security Metrics to Track:
- Authentication success rate: Target >99%
- Rate limit effectiveness: 429 errors <5% for legitimate traffic
- Webhook delivery success: >98% first-attempt delivery
- Mean time to detect (MTTD): <5 minutes for security incidents
- Mean time to respond (MTTR): <15 minutes for critical incidents
Next Steps:
For GraphQL APIs, implement query depth limiting, cost analysis, and field-level authorization. For gRPC services, enforce mTLS and implement interceptor-based authorization. Consider advanced threat protection with machine learning anomaly detection and behavioral analysis for high-value APIs.
Implement zero trust architecture using service mesh security (Istio, Linkerd) with workload identity (SPIFFE/SPIRE) for microservices environments. Continuous verification and east-west traffic encryption provide defense-in-depth protection.
The API security landscape evolves rapidly. Stay current with OWASP API Security Project updates, OAuth working group developments, and emerging threat intelligence. Regular security audits, penetration testing, and compliance assessments ensure ongoing protection.
Related Tools
This workflow leverages 10 specialized tools:
Authentication & Authorization:
- OAuth/OIDC Debugger - Design OAuth flows, generate PKCE challenges
- JWT Decoder - Decode tokens, validate claims, detect security issues
API Development:
- HTTP Request Builder - Test authentication, analyze responses
- HTTP Status Codes - Understand retry semantics
Security:
- Rate Limit Calculator - Model algorithms, calculate optimal limits
- CORS Policy Analyzer - Validate CORS configurations
- Security Headers Analyzer - Audit HTTP security headers
Webhooks:
- Webhook Tester & Inspector - Capture payloads, validate signatures
- Webhook Payload Generator - Generate signed test payloads
Threat Intelligence:
- IP Risk Checker - Geolocation, reputation scoring, VPN detection
