API Authentication & Security: Complete Implementation Guide
API Authentication & Security: Complete Implementation Guide
Securing your API isn't optional—it's the foundation of trust between you and your users. A single authentication vulnerability can expose user data, enable unauthorized access, and destroy years of reputation building.
This guide covers production-ready authentication patterns used by Auth0, AWS Cognito, Okta, and Stripe. You'll learn when to use each method, how to implement them securely, and common pitfalls to avoid.
Table of Contents
- Authentication vs Authorization
- Authentication Methods Comparison
- API Keys
- JWT (JSON Web Tokens)
- OAuth 2.0
- Security Best Practices
- Implementation Patterns
- Common Security Vulnerabilities
- Testing & Monitoring
- Production Checklist
Authentication vs Authorization
Before diving into implementation, understand the difference:
- Authentication = "Who are you?" (proving identity)
- Authorization = "What can you do?" (checking permissions)
Example flow:
// 1. Authentication: Verify the user is who they claim to be
const user = await verifyAuthToken(request.headers.authorization);
// 2. Authorization: Check if they can perform this action
if (!user.permissions.includes('write:posts')) {
throw new ForbiddenError('Insufficient permissions');
}
// 3. Proceed with the request
await createPost(user.id, request.body);
Authentication Methods Comparison
| Method | Best For | Security Level | Complexity | Revocation |
|---|---|---|---|---|
| API Keys | Server-to-server, simple auth | Medium | Low | Easy |
| JWT | Stateless auth, mobile/SPA apps | High | Medium | Hard* |
| OAuth 2.0 | Third-party integrations | High | High | Easy |
| Basic Auth | Internal tools, prototypes | Low | Very Low | N/A |
*JWT revocation requires additional infrastructure (deny lists, short expiry + refresh tokens)
When to Use Each Method
Use API Keys when:
- Server-to-server communication (backend → API)
- Simple authentication needs
- You control both client and server
- Example: Stripe API, SendGrid
Use JWT when:
- Building single-page apps (SPAs) or mobile apps
- You need stateless authentication
- Session data needs to be stored in the token
- Example: Internal application auth
Use OAuth 2.0 when:
- Third-party apps need access to your API
- "Login with Google/GitHub" flows
- Granular permission scopes required
- Example: GitHub API, Slack API
API Keys
What Are API Keys?
API keys are unique identifiers assigned to API consumers. They're the simplest authentication method.
Stripe API key example:
curl https://api.stripe.com/v1/charges \
-u sk_test_4eC39HqLyjWDarjtT1zdp7dc:
Implementing API Keys
1. Generate Secure Keys
import crypto from 'crypto';
function generateApiKey(): string {
// Generate 32 random bytes, convert to hex (64 characters)
return crypto.randomBytes(32).toString('hex');
}
// Example output: "a7f3e2b9c4d1f8e5a3b7c9d2e4f6a8b1c3d5e7f9a1b3c5d7e9f1a3b5c7d9e1f3"
Security requirements:
- Minimum 32 characters
- Cryptographically random (not Math.random())
- Store hashed version in database (like passwords)
2. Hash Keys Before Storage
import bcrypt from 'bcrypt';
async function hashApiKey(apiKey: string): Promise<string> {
const saltRounds = 10;
return bcrypt.hash(apiKey, saltRounds);
}
// When creating a new API key:
const apiKey = generateApiKey();
const hashedKey = await hashApiKey(apiKey);
// Store hashedKey in database
await db.apiKeys.create({
userId: user.id,
keyHash: hashedKey,
name: 'Production API Key',
createdAt: new Date(),
});
// Show apiKey to user ONCE (they can't retrieve it later)
return { apiKey }; // Display this to user, never show again
3. Verify API Keys
import { Request, Response, NextFunction } from 'express';
async function authenticateApiKey(
req: Request,
res: Response,
next: NextFunction
) {
const apiKey = req.headers['x-api-key'] as string;
if (!apiKey) {
return res.status(401).json({
error: 'API key required',
hint: 'Include X-API-Key header',
});
}
// Find all active API keys (hashed)
const apiKeys = await db.apiKeys.findMany({
where: { active: true },
});
// Check if provided key matches any hashed key
let matchedKey = null;
for (const key of apiKeys) {
const isMatch = await bcrypt.compare(apiKey, key.keyHash);
if (isMatch) {
matchedKey = key;
break;
}
}
if (!matchedKey) {
return res.status(401).json({
error: 'Invalid API key',
});
}
// Update last used timestamp
await db.apiKeys.update({
where: { id: matchedKey.id },
data: { lastUsedAt: new Date() },
});
// Attach user to request
req.user = await db.users.findUnique({
where: { id: matchedKey.userId },
});
next();
}
API Key Best Practices
1. Use separate keys for different environments:
// Prefix keys to prevent accidental usage
const testKey = `test_${generateApiKey()}`;
const prodKey = `prod_${generateApiKey()}`;
2. Allow multiple keys per user: Users should be able to create separate keys for different applications and rotate them independently.
3. Implement key rotation:
interface ApiKey {
id: string;
userId: string;
keyHash: string;
name: string;
createdAt: Date;
lastUsedAt: Date | null;
expiresAt: Date | null;
active: boolean;
}
// Automatically expire old keys
async function deactivateExpiredKeys() {
await db.apiKeys.updateMany({
where: {
expiresAt: { lte: new Date() },
active: true,
},
data: { active: false },
});
}
4. Rate limit by API key: Track usage per key and enforce rate limits (see our API Rate Limiting Guide).
JWT (JSON Web Tokens)
What is JWT?
JWT is a compact, self-contained way to securely transmit information between parties. The token itself contains the user's identity and permissions.
Structure:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Decodes to:
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
// Signature (verifies integrity)
Implementing JWT Authentication
1. Generate JWT Tokens
import jwt from 'jsonwebtoken';
interface TokenPayload {
userId: string;
email: string;
role: string;
}
function generateAccessToken(user: User): string {
const payload: TokenPayload = {
userId: user.id,
email: user.email,
role: user.role,
};
// Short-lived access token (15 minutes)
return jwt.sign(payload, process.env.JWT_SECRET!, {
expiresIn: '15m',
issuer: 'api.yourcompany.com',
audience: 'api.yourcompany.com',
});
}
function generateRefreshToken(userId: string): string {
// Long-lived refresh token (7 days)
return jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);
}
Security requirements:
- Store
JWT_SECRETin environment variables - Use strong secrets (minimum 256 bits)
- Generate with:
openssl rand -base64 32
2. Verify JWT Tokens
async function authenticateJWT(
req: Request,
res: Response,
next: NextFunction
) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Authentication required',
hint: 'Include Authorization: Bearer <token> header',
});
}
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
try {
const payload = jwt.verify(
token,
process.env.JWT_SECRET!
) as TokenPayload;
// Optionally: Check if user still exists and is active
const user = await db.users.findUnique({
where: { id: payload.userId },
});
if (!user || !user.active) {
throw new Error('User not found or inactive');
}
req.user = user;
next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).json({
error: 'Token expired',
code: 'TOKEN_EXPIRED',
hint: 'Refresh your access token',
});
}
if (error instanceof jwt.JsonWebTokenError) {
return res.status(401).json({
error: 'Invalid token',
code: 'INVALID_TOKEN',
});
}
return res.status(500).json({
error: 'Authentication failed',
});
}
}
3. Refresh Token Flow
Access tokens should be short-lived (15 minutes). Use refresh tokens to get new access tokens without re-authenticating.
// POST /auth/refresh
async function refreshAccessToken(req: Request, res: Response) {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({
error: 'Refresh token required',
});
}
try {
// Verify refresh token
const payload = jwt.verify(
refreshToken,
process.env.JWT_REFRESH_SECRET!
) as { userId: string };
// Check if refresh token is in deny list (if implemented)
const isDenied = await isRefreshTokenDenied(refreshToken);
if (isDenied) {
throw new Error('Token revoked');
}
// Get user
const user = await db.users.findUnique({
where: { id: payload.userId },
});
if (!user || !user.active) {
throw new Error('User not found');
}
// Generate new access token
const newAccessToken = generateAccessToken(user);
return res.json({
accessToken: newAccessToken,
});
} catch (error) {
return res.status(401).json({
error: 'Invalid refresh token',
hint: 'Please log in again',
});
}
}
JWT Security Best Practices
1. Use HTTPS only: Tokens transmitted over HTTP can be intercepted.
2. Store tokens securely on client:
// ❌ BAD: localStorage is vulnerable to XSS
localStorage.setItem('token', accessToken);
// ✅ GOOD: httpOnly cookies (not accessible to JavaScript)
res.cookie('accessToken', accessToken, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 15 * 60 * 1000, // 15 minutes
});
3. Implement token deny lists for logout:
// Store logged-out tokens in Redis with TTL = token expiry
async function logoutUser(token: string) {
const decoded = jwt.decode(token) as { exp: number };
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
await redis.setex(`denied:${token}`, ttl, '1');
}
async function isTokenDenied(token: string): Promise<boolean> {
const denied = await redis.get(`denied:${token}`);
return denied === '1';
}
4. Include token metadata:
const payload = {
userId: user.id,
email: user.email,
role: user.role,
iat: Math.floor(Date.now() / 1000), // Issued at
exp: Math.floor(Date.now() / 1000) + 900, // Expires in 15 min
jti: crypto.randomUUID(), // Unique token ID
};
OAuth 2.0
OAuth 2.0 is an authorization framework that enables third-party applications to access user resources without sharing passwords.
OAuth 2.0 Flows
1. Authorization Code Flow (most secure, for web apps) 2. Client Credentials Flow (machine-to-machine) 3. Implicit Flow (deprecated, don't use) 4. Password Flow (only for trusted first-party apps)
Authorization Code Flow (Recommended)
This is what "Login with Google" uses. Example: building a Slack bot that posts to user channels.
Flow:
User → Your App → Slack OAuth → User Approves → Slack → Your App → Access Token
Implementation:
import axios from 'axios';
// Step 1: Redirect user to authorization URL
app.get('/auth/slack', (req, res) => {
const authUrl = new URL('https://slack.com/oauth/v2/authorize');
authUrl.searchParams.set('client_id', process.env.SLACK_CLIENT_ID!);
authUrl.searchParams.set('scope', 'chat:write,channels:read');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/auth/slack/callback');
authUrl.searchParams.set('state', generateRandomState()); // CSRF protection
res.redirect(authUrl.toString());
});
// Step 2: Handle callback with authorization code
app.get('/auth/slack/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state matches (CSRF protection)
if (!verifyState(state as string)) {
return res.status(400).send('Invalid state');
}
try {
// Step 3: Exchange code for access token
const response = await axios.post('https://slack.com/api/oauth.v2.access', {
client_id: process.env.SLACK_CLIENT_ID,
client_secret: process.env.SLACK_CLIENT_SECRET,
code,
redirect_uri: 'https://yourapp.com/auth/slack/callback',
});
const { access_token, team, authed_user } = response.data;
// Step 4: Store access token securely
await db.oauthTokens.create({
data: {
userId: req.session.userId, // Your app's user
provider: 'slack',
accessToken: encrypt(access_token), // Encrypt before storage
teamId: team.id,
slackUserId: authed_user.id,
scopes: 'chat:write,channels:read',
createdAt: new Date(),
},
});
res.redirect('/dashboard?connected=slack');
} catch (error) {
console.error('OAuth error:', error);
res.status(500).send('Authentication failed');
}
});
// Step 5: Use the access token to make API requests
async function postToSlack(userId: string, channel: string, text: string) {
const token = await db.oauthTokens.findFirst({
where: { userId, provider: 'slack' },
});
if (!token) {
throw new Error('Slack not connected');
}
const decryptedToken = decrypt(token.accessToken);
await axios.post(
'https://slack.com/api/chat.postMessage',
{ channel, text },
{
headers: {
'Authorization': `Bearer ${decryptedToken}`,
'Content-Type': 'application/json',
},
}
);
}
OAuth Security Best Practices
1. Always use state parameter (CSRF protection):
function generateRandomState(): string {
return crypto.randomBytes(32).toString('hex');
}
// Store in session
req.session.oauthState = generateRandomState();
// Verify on callback
function verifyState(receivedState: string): boolean {
const valid = receivedState === req.session.oauthState;
delete req.session.oauthState; // Use once
return valid;
}
2. Use PKCE for additional security:
// For mobile apps and SPAs
function generateCodeVerifier(): string {
return crypto.randomBytes(32).toString('base64url');
}
function generateCodeChallenge(verifier: string): string {
return crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
}
3. Validate redirect URIs: Never allow arbitrary redirect URIs. Whitelist allowed URLs in your OAuth app configuration.
4. Encrypt stored tokens:
import crypto from 'crypto';
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; // 32 bytes
const IV_LENGTH = 16;
function encrypt(text: string): string {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
function decrypt(text: string): string {
const parts = text.split(':');
const iv = Buffer.from(parts[0], 'hex');
const encryptedText = parts[1];
const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
Security Best Practices
1. Always Use HTTPS
Never send authentication credentials over HTTP.
// Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') {
return res.redirect(`https://${req.headers.host}${req.url}`);
}
next();
});
2. Implement Rate Limiting
Prevent brute force attacks on authentication endpoints.
import rateLimit from 'express-rate-limit';
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: {
error: 'Too many authentication attempts',
hint: 'Please try again in 15 minutes',
},
standardHeaders: true,
legacyHeaders: false,
});
app.post('/auth/login', authLimiter, async (req, res) => {
// Login logic
});
See our complete API Rate Limiting Guide for production patterns.
3. Hash Passwords Properly
import bcrypt from 'bcrypt';
// Creating user
async function createUser(email: string, password: string) {
const hashedPassword = await bcrypt.hash(password, 10);
await db.users.create({
data: {
email,
password: hashedPassword,
},
});
}
// Verifying password
async function verifyPassword(email: string, password: string): Promise<User | null> {
const user = await db.users.findUnique({ where: { email } });
if (!user) {
// Don't reveal if user exists
await bcrypt.hash(password, 10); // Prevent timing attacks
return null;
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return null;
}
return user;
}
4. Implement CORS Properly
import cors from 'cors';
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'https://yourapp.com',
credentials: true, // Allow cookies
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
}));
5. Add Security Headers
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
}));
6. Validate Input
import { z } from 'zod';
const LoginSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = LoginSchema.parse(req.body);
// Proceed with authentication
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors,
});
}
}
});
7. Log Security Events
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'security.log' }),
],
});
// Log authentication attempts
logger.info('Authentication attempt', {
email: user.email,
ip: req.ip,
userAgent: req.headers['user-agent'],
success: true,
timestamp: new Date().toISOString(),
});
// Log suspicious activity
logger.warn('Multiple failed login attempts', {
email: email,
ip: req.ip,
attempts: 5,
timestamp: new Date().toISOString(),
});
Common Security Vulnerabilities
1. Exposed Secrets in Code
// ❌ NEVER do this
const API_KEY = 'sk_live_abc123';
// ✅ Use environment variables
const API_KEY = process.env.API_KEY;
if (!API_KEY) {
throw new Error('API_KEY environment variable required');
}
Prevention:
- Use
.envfiles (never commit them) - Use secret management (AWS Secrets Manager, HashiCorp Vault)
- Scan code with tools like
git-secretsortrufflehog
2. Timing Attacks
// ❌ Vulnerable to timing attacks
function compareTokens(userToken: string, validToken: string): boolean {
return userToken === validToken; // Returns immediately when first character differs
}
// ✅ Use constant-time comparison
import crypto from 'crypto';
function compareTokens(userToken: string, validToken: string): boolean {
const userBuffer = Buffer.from(userToken);
const validBuffer = Buffer.from(validToken);
if (userBuffer.length !== validBuffer.length) {
return false;
}
return crypto.timingSafeEqual(userBuffer, validBuffer);
}
3. Insecure Token Storage
// ❌ NEVER store tokens in localStorage (vulnerable to XSS)
localStorage.setItem('token', accessToken);
// ✅ Use httpOnly cookies
res.cookie('accessToken', accessToken, {
httpOnly: true, // Not accessible to JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 15 * 60 * 1000, // 15 minutes
});
4. Missing Token Expiration
// ❌ Tokens that never expire
const token = jwt.sign({ userId: user.id }, SECRET);
// ✅ Short-lived access tokens + refresh tokens
const accessToken = jwt.sign(
{ userId: user.id },
SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
5. Weak Secrets
# ❌ Weak secret
JWT_SECRET=secret123
# ✅ Generate strong secret (256 bits minimum)
openssl rand -base64 32
# Output: 8Zr3vN9mX2fQ7jK5pL1wT6yU4iO0sA9dF8gH3bC7vN2=
Testing & Monitoring
Testing Authentication
import request from 'supertest';
describe('Authentication', () => {
it('should reject requests without token', async () => {
const response = await request(app)
.get('/api/protected')
.expect(401);
expect(response.body.error).toBe('Authentication required');
});
it('should reject invalid tokens', async () => {
const response = await request(app)
.get('/api/protected')
.set('Authorization', 'Bearer invalid_token')
.expect(401);
expect(response.body.code).toBe('INVALID_TOKEN');
});
it('should accept valid tokens', async () => {
const user = await createTestUser();
const token = generateAccessToken(user);
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200);
});
it('should reject expired tokens', async () => {
const expiredToken = jwt.sign(
{ userId: 'test' },
process.env.JWT_SECRET!,
{ expiresIn: '-1s' } // Already expired
);
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
expect(response.body.code).toBe('TOKEN_EXPIRED');
});
});
Monitor Authentication Metrics
Track these metrics with Datadog, New Relic, or Sentry:
1. Failed authentication attempts:
// Alert when >100 failed attempts in 5 minutes
if (failedAttempts > 100) {
alertSecurityTeam('Potential brute force attack');
}
2. Token expiration errors:
// High rate of TOKEN_EXPIRED errors may indicate:
// - Tokens expiring too quickly
// - Refresh flow not working
// - Clock skew issues
3. Unusual access patterns:
// Alert on:
// - Same token used from multiple IPs simultaneously
// - Tokens used after user logout
// - Tokens used from unexpected geolocations
Production Checklist
Before launching your authentication system:
Security
- All secrets stored in environment variables or secret manager
- HTTPS enforced (HTTP redirects to HTTPS)
- Passwords hashed with bcrypt (salt rounds ≥10)
- API keys stored hashed in database
- JWT secrets are 256+ bits
- Access tokens expire (15 minutes recommended)
- Refresh tokens expire (7 days recommended)
- Rate limiting on authentication endpoints (5 attempts / 15 min)
- CORS configured with specific origins (not
*) - Security headers implemented (Helmet.js)
- Input validation on all authentication endpoints
- Constant-time comparison for tokens
- httpOnly cookies for token storage
- CSRF protection (state parameter for OAuth)
- XSS protection (Content Security Policy)
- OAuth tokens encrypted before database storage
Monitoring
- Failed authentication attempts logged
- Successful logins logged with IP/user-agent
- Token expiration errors tracked
- Unusual access patterns alerted
- Security logs sent to Datadog/Sentry
- Dashboard showing auth success/failure rates
User Experience
- Clear error messages (without leaking info)
- Token refresh flow implemented
- Logout invalidates tokens
- Password reset flow implemented
- Multi-factor authentication considered
- Remember me option (optional)
Documentation
- Authentication methods documented
- API key generation process documented
- Token format and claims documented
- Error codes documented
- Rate limits communicated
- Security best practices shared with API consumers
Real-World Examples
Stripe's API Key Pattern
Stripe uses prefixed API keys to prevent accidental misuse:
sk_test_... // Secret key, test mode
sk_live_... // Secret key, production
pk_test_... // Publishable key, test mode
pk_live_... // Publishable key, production
Benefits:
- Can't accidentally use test keys in production
- Immediately identify key type
- Can implement different validation logic per prefix
Auth0's JWT Claims
Auth0 includes rich metadata in JWT tokens:
{
"sub": "auth0|507f1f77bcf86cd799439011",
"email": "user@example.com",
"email_verified": true,
"iss": "https://yourapp.auth0.com/",
"aud": "your_client_id",
"iat": 1516239022,
"exp": 1516325422,
"scope": "openid profile email",
"https://yourapp.com/roles": ["admin", "user"]
}
Custom claims use namespaced keys to avoid collisions.
GitHub's OAuth Scopes
GitHub uses granular permission scopes:
repo # Full repo access
repo:status # Just commit status
public_repo # Public repos only
read:org # Read org data
write:discussion # Create discussions
Users see exactly what permissions your app requests.
Conclusion
Securing your API is a continuous process:
- Start with the right method — API keys for simple cases, JWT for stateless auth, OAuth for third-party access
- Follow security best practices — HTTPS, rate limiting, strong secrets, input validation
- Monitor actively — Track failed attempts, unusual patterns, token usage
- Iterate — Add MFA, refine scopes, improve error messages
Real-world API authentication from Auth0, Okta, AWS Cognito, and Stripe follows these patterns. Learn from their implementations and adapt to your needs.
For more API reliability patterns:
- API Error Handling Best Practices
- API Rate Limiting Complete Guide
- Complete Webhook Implementation Guide
- API Dependency Monitoring Strategy
Monitor authentication provider uptime: Auth0 Status • Okta Status • AWS Status • Firebase Status • Clerk Status
API Status Check
Stop checking API status pages manually
Get instant email alerts when OpenAI, Stripe, AWS, and 100+ APIs go down. Know before your users do.
Free dashboard available · 14-day trial on paid plans · Cancel anytime
Browse Free Dashboard →