Screenshot API Security Best Practices: Authentication, Rate Limiting, and Data Protection
Secure your screenshot automation with proper API key handling, rate limiting, and data protection strategies.
Screenshot APIs can access any public URL and potentially capture sensitive information. Proper security practices protect your API keys, prevent abuse, and ensure data privacy.
This guide covers security best practices for screenshot API integration at every level.
API Key Management
Never Hardcode Keys
The #1 security mistake:
// ❌ NEVER do this
const API_KEY = 'sk_live_abc123xyz';
// ✅ Use environment variables
const API_KEY = process.env.SCREENSHOTLY_API_KEY;
Environment Variable Security
# Development: .env file (never commit!)
SCREENSHOTLY_API_KEY=sk_test_xxx
# Add to .gitignore
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo ".env*.local" >> .gitignore
Production Key Management
Use secrets managers in production:
AWS Secrets Manager:
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
async function getApiKey() {
const client = new SecretsManager();
const response = await client.getSecretValue({ SecretId: 'screenshotly-api-key' });
return JSON.parse(response.SecretString).apiKey;
}
HashiCorp Vault:
import Vault from 'node-vault';
async function getApiKey() {
const vault = Vault({ endpoint: process.env.VAULT_ADDR });
const result = await vault.read('secret/data/screenshotly');
return result.data.data.apiKey;
}
Kubernetes Secrets:
apiVersion: v1
kind: Secret
metadata:
name: screenshotly-secrets
type: Opaque
data:
api-key: c2tfbGl2ZV94eHg= # base64 encoded
Key Rotation
Rotate API keys regularly:
// Support multiple keys during rotation
const API_KEYS = {
current: process.env.SCREENSHOTLY_API_KEY,
previous: process.env.SCREENSHOTLY_API_KEY_PREVIOUS,
};
async function captureWithFallback(url) {
try {
return await capture(url, API_KEYS.current);
} catch (error) {
if (error.status === 401 && API_KEYS.previous) {
console.warn('Current key failed, trying previous');
return await capture(url, API_KEYS.previous);
}
throw error;
}
}
Input Validation
URL Validation
Never trust user-provided URLs:
function validateUrl(url) {
// Check URL format
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error('Invalid URL format');
}
// Only allow HTTP/HTTPS
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('Only HTTP/HTTPS URLs allowed');
}
// Block internal networks
const hostname = parsed.hostname;
if (isInternalNetwork(hostname)) {
throw new Error('Internal URLs not allowed');
}
// Block file:// and data: URLs
if (parsed.protocol === 'file:' || url.startsWith('data:')) {
throw new Error('File and data URLs not allowed');
}
return parsed.href;
}
function isInternalNetwork(hostname) {
const internalPatterns = [
/^localhost$/i,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./,
/^169\.254\./,
/^::1$/,
/^fc00:/i,
/^fe80:/i,
/\.local$/i,
/\.internal$/i,
];
return internalPatterns.some(pattern => pattern.test(hostname));
}
SSRF Prevention
Server-Side Request Forgery is a major risk:
// Additional SSRF protections
async function safeCapture(url) {
const validatedUrl = validateUrl(url);
// Resolve DNS to check actual IP
const addresses = await dns.promises.resolve4(new URL(validatedUrl).hostname);
for (const ip of addresses) {
if (isPrivateIP(ip)) {
throw new Error('URL resolves to private IP');
}
}
return await captureScreenshot(validatedUrl);
}
function isPrivateIP(ip) {
const parts = ip.split('.').map(Number);
// 10.x.x.x
if (parts[0] === 10) return true;
// 172.16.x.x - 172.31.x.x
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
// 192.168.x.x
if (parts[0] === 192 && parts[1] === 168) return true;
// 127.x.x.x
if (parts[0] === 127) return true;
return false;
}
Domain Allowlisting
For highest security, only allow specific domains:
const ALLOWED_DOMAINS = [
'example.com',
'app.example.com',
'docs.example.com',
];
function isDomainAllowed(url) {
const hostname = new URL(url).hostname;
return ALLOWED_DOMAINS.some(domain =>
hostname === domain || hostname.endsWith('.' + domain)
);
}
Rate Limiting
Client-Side Rate Limiting
Protect yourself from accidental overuse:
import Bottleneck from 'bottleneck';
const limiter = new Bottleneck({
maxConcurrent: 5, // Max 5 concurrent requests
minTime: 200, // Min 200ms between requests
reservoir: 100, // Max 100 requests
reservoirRefreshAmount: 100,
reservoirRefreshInterval: 60 * 1000, // Per minute
});
async function rateLimitedCapture(url) {
return limiter.schedule(() => captureScreenshot(url));
}
Handle API Rate Limits
Respect 429 responses:
async function captureWithRateLimit(url) {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url }),
});
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
console.log(`Rate limited, waiting ${retryAfter}s`);
await sleep(parseInt(retryAfter) * 1000);
return captureWithRateLimit(url); // Retry
}
return response;
}
Budget Controls
Set spending limits:
class BudgetTracker {
constructor(dailyLimit) {
this.dailyLimit = dailyLimit;
this.usage = 0;
this.resetAt = this.getNextReset();
}
getNextReset() {
const tomorrow = new Date();
tomorrow.setHours(0, 0, 0, 0);
tomorrow.setDate(tomorrow.getDate() + 1);
return tomorrow;
}
async checkBudget() {
if (new Date() > this.resetAt) {
this.usage = 0;
this.resetAt = this.getNextReset();
}
if (this.usage >= this.dailyLimit) {
throw new Error('Daily screenshot budget exceeded');
}
this.usage++;
return true;
}
}
const budget = new BudgetTracker(1000); // 1000/day limit
async function budgetedCapture(url) {
await budget.checkBudget();
return captureScreenshot(url);
}
Data Protection
Screenshot Storage Security
Protect stored screenshots:
import crypto from 'crypto';
// Encrypt before storage
function encryptScreenshot(buffer, key) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([
cipher.update(buffer),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
return Buffer.concat([iv, authTag, encrypted]);
}
// Decrypt when needed
function decryptScreenshot(encryptedBuffer, key) {
const iv = encryptedBuffer.slice(0, 16);
const authTag = encryptedBuffer.slice(16, 32);
const data = encryptedBuffer.slice(32);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
return Buffer.concat([
decipher.update(data),
decipher.final(),
]);
}
Automatic Cleanup
Don't keep screenshots longer than needed:
// S3 lifecycle policy
{
"Rules": [
{
"ID": "DeleteOldScreenshots",
"Status": "Enabled",
"Filter": { "Prefix": "screenshots/" },
"Expiration": { "Days": 30 }
}
]
}
// Or programmatic cleanup
async function cleanupOldScreenshots() {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 30);
const oldScreenshots = await db.screenshots
.where('createdAt')
.lt(cutoff)
.toArray();
for (const screenshot of oldScreenshots) {
await storage.delete(screenshot.path);
await db.screenshots.delete(screenshot.id);
}
}
Access Logging
Track who accesses screenshots:
async function getScreenshot(id, userId) {
const screenshot = await db.screenshots.get(id);
if (!screenshot) {
throw new Error('Screenshot not found');
}
// Check access permissions
if (!hasAccess(userId, screenshot)) {
await logUnauthorizedAccess(userId, id);
throw new Error('Access denied');
}
// Log access
await db.accessLogs.insert({
screenshotId: id,
userId,
action: 'view',
timestamp: new Date(),
ip: getClientIP(),
});
return screenshot;
}
Sensitive Content Handling
Content Warnings
Screenshots may capture sensitive data:
// Warn before capturing financial sites
const SENSITIVE_PATTERNS = [
/bank/i,
/payment/i,
/checkout/i,
/account/i,
/login/i,
/signin/i,
];
function checkSensitiveUrl(url) {
const isSensitive = SENSITIVE_PATTERNS.some(p => p.test(url));
if (isSensitive) {
console.warn(`⚠️ Capturing potentially sensitive URL: ${url}`);
// Consider additional protections
}
return isSensitive;
}
PII Detection
Consider scanning screenshots for sensitive data:
// Placeholder for OCR + PII detection
async function checkForPII(screenshotBuffer) {
const text = await performOCR(screenshotBuffer);
const patterns = {
ssn: /\d{3}-\d{2}-\d{4}/,
creditCard: /\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}/,
email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/,
};
const found = [];
for (const [type, pattern] of Object.entries(patterns)) {
if (pattern.test(text)) {
found.push(type);
}
}
if (found.length > 0) {
console.warn(`PII detected: ${found.join(', ')}`);
// Consider redaction or alerting
}
return found;
}
Audit Trail
Comprehensive Logging
Log all screenshot activity:
async function auditedCapture(url, userId, purpose) {
const auditId = crypto.randomUUID();
const startTime = Date.now();
try {
// Log request
await audit.log({
id: auditId,
action: 'screenshot_request',
userId,
url,
purpose,
timestamp: new Date(),
ip: getClientIP(),
});
const result = await captureScreenshot(url);
// Log success
await audit.log({
id: auditId,
action: 'screenshot_success',
duration: Date.now() - startTime,
size: result.length,
});
return result;
} catch (error) {
// Log failure
await audit.log({
id: auditId,
action: 'screenshot_failure',
error: error.message,
duration: Date.now() - startTime,
});
throw error;
}
}
Security Checklist
Before Deployment
- API keys stored in environment/secrets manager
-
.envfiles in.gitignore - URL validation implemented
- SSRF protections in place
- Rate limiting configured
- Budget controls set
- Access logging enabled
- Screenshot encryption considered
- Retention policies defined
- Audit logging implemented
Ongoing
- Rotate API keys quarterly
- Review access logs monthly
- Audit URL patterns allowed
- Monitor for anomalies
- Update dependencies
- Test security controls
Conclusion
Screenshot API security requires attention at every layer:
- Key management - Never expose API keys
- Input validation - Validate and sanitize URLs
- Rate limiting - Prevent abuse and cost overruns
- Data protection - Encrypt and expire screenshots
- Audit logging - Track all screenshot activity
Implement these practices from day one—security becomes harder to add later.
Ready for secure screenshot automation?
Get your free API key → - 100 free screenshots to get started.
See also: Error Handling Guide →
About the Author

Asad Ali
Full-Stack Developer and Founder of ZTabs with 8+ years of experience building scalable web applications and APIs. Specializes in performance optimization, SaaS development, and modern web technologies.
Credentials: Founder & CEO at ZTabs, Full-Stack Developer, Expert in Next.js, React, Node.js, and API optimization
Ready to capture your first screenshot?
Get started with 100 free screenshots. No credit card required.