## What Are Signed URLs? A signed URL is a regular URL with a cryptographic signature appended as a query parameter. It proves the request was authorized without exposing your API key. The signature is generated server-side using your secret key, but the URL can be used client-side in `` tags, emails, or anywhere that loads images. ```html ``` ## Why Use Signed URLs? ### The Problem with API Keys in Frontend You can't put API keys in client-side code: ```html ``` ### The Traditional Workaround Build a backend proxy that forwards requests: ```javascript // Server-side proxy app.get('/api/screenshot', async (req, res) => { const response = await axios.get('https://api.toolcenter.dev/v1/screenshot', { params: { url: req.query.url, width: 1280, height: 800 }, headers: { 'Authorization': 'Bearer YOUR_API_KEY' }, responseType: 'arraybuffer', }); res.set('Content-Type', 'image/png'); res.send(response.data); }); ``` This works but adds latency, server load, and complexity. ### The Signed URL Solution Generate the signed URL server-side, use it anywhere client-side: ```javascript // Server: generate signed URL (fast, no screenshot taken yet) const signedUrl = generateSignedUrl('https://example.com', { width: 1280, height: 800 }); // Client: use it directly in HTML // ``` The screenshot is taken only when the browser loads the image. Your server does zero proxying. ## Generating Signed URLs ### Node.js ```javascript const crypto = require('crypto'); function generateSignedUrl(targetUrl, options = {}) { const params = new URLSearchParams({ url: targetUrl, width: options.width || 1280, height: options.height || 800, format: options.format || 'png', // Optional: expiration timestamp expires: options.expires || Math.floor(Date.now() / 1000) + 3600, // 1 hour }); // Create signature from all parameters const signature = crypto .createHmac('sha256', process.env.DEVTOOLBOX_SIGNING_SECRET) .update(params.toString()) .digest('hex'); params.set('sig', signature); return `https://api.toolcenter.dev/v1/screenshot?${params.toString()}`; } // Generate a signed URL const signedUrl = generateSignedUrl('https://example.com', { width: 1280, height: 800, expires: Math.floor(Date.now() / 1000) + 86400, // 24 hours }); console.log(signedUrl); ``` ### Python ```python import hmac import hashlib import time from urllib.parse import urlencode import os def generate_signed_url(target_url, width=1280, height=800, ttl=3600): params = { 'url': target_url, 'width': width, 'height': height, 'format': 'png', 'expires': int(time.time()) + ttl, } # Create signature query_string = urlencode(sorted(params.items())) signature = hmac.new( os.environ['DEVTOOLBOX_SIGNING_SECRET'].encode(), query_string.encode(), hashlib.sha256 ).hexdigest() params['sig'] = signature return f"https://api.toolcenter.dev/v1/screenshot?{urlencode(params)}" # Generate URL valid for 24 hours url = generate_signed_url('https://example.com', ttl=86400) print(url) ``` ## Using Signed URLs in HTML ### Simple Image Embed ```html Screenshot of example.com ``` ### Link Preview Cards ```html ``` ### Email Embeds Signed URLs work in email since they're just standard image URLs: ```html
Website preview
``` ## Security Best Practices ### 1. Always Set Expiration Never create signed URLs without an expiration: ```javascript const expires = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now ``` Use short TTLs for sensitive content and longer TTLs for public content. ### 2. Restrict URL Patterns Limit which URLs can be screenshotted to prevent abuse: ```javascript function generateSignedUrl(targetUrl, options = {}) { // Only allow screenshots of approved domains const allowedDomains = ['example.com', 'docs.example.com', 'blog.example.com']; const urlObj = new URL(targetUrl); if (!allowedDomains.includes(urlObj.hostname)) { throw new Error(`Domain not allowed: ${urlObj.hostname}`); } // ... generate signature } ``` ### 3. Rate Limit by Signature Track how many times each signed URL is used: ```javascript const usageTracker = new Map(); const MAX_USES = 10; function checkUsage(signature) { const count = usageTracker.get(signature) || 0; if (count >= MAX_USES) { throw new Error('Signed URL usage limit exceeded'); } usageTracker.set(signature, count + 1); } ``` ### 4. Rotate Signing Secrets Periodically rotate your signing secret. During rotation, accept both old and new secrets: ```javascript function verifySignature(params, signature) { const secrets = [ process.env.DEVTOOLBOX_SIGNING_SECRET, process.env.DEVTOOLBOX_SIGNING_SECRET_PREVIOUS, ].filter(Boolean); return secrets.some(secret => { const expected = crypto.createHmac('sha256', secret).update(params).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); }); } ``` ## Server-Side Integration ### Express.js Endpoint Create an endpoint that generates signed URLs for your frontend: ```javascript app.get('/api/preview-url', (req, res) => { const { url } = req.query; if (!url) { return res.status(400).json({ error: 'URL required' }); } const signedUrl = generateSignedUrl(url, { width: 1200, height: 630, expires: Math.floor(Date.now() / 1000) + 86400, }); res.json({ previewUrl: signedUrl }); }); ``` ### Next.js API Route ```javascript // pages/api/preview.js export default function handler(req, res) { const { url } = req.query; const signedUrl = generateSignedUrl(url, { width: 1200, height: 630 }); res.json({ previewUrl: signedUrl }); } ``` ## Caching Considerations Signed URLs with the same parameters generate the same signature, making them cache-friendly: ```html ``` Add cache headers on your CDN to avoid regenerating the same screenshot: ``` Cache-Control: public, max-age=86400 ``` For dynamic content that changes frequently, use shorter expiration times and cache-busting parameters: ```javascript const signedUrl = generateSignedUrl(url, { expires: Math.floor(Date.now() / 1000) + 300, // 5 minutes cacheBust: Date.now(), // Unique per request }); ``` ## Conclusion Signed URLs are the cleanest way to embed live screenshots in HTML. They keep your API key secure, eliminate the need for backend proxying, and work everywhere that images work — web pages, emails, markdown, and more. Generate them server-side with a short expiration, and use them freely on the client.