import { Router } from "express"; import { takeScreenshot } from "../services/screenshot.js"; import logger from "../services/logger.js"; export const screenshotRouter = Router(); /** * @openapi * /v1/screenshot: * post: * tags: [Screenshots] * summary: Take a screenshot (authenticated) * description: Capture a pixel-perfect, unwatermarked screenshot. Requires an API key. * operationId: takeScreenshot * security: * - BearerAuth: [] * - ApiKeyAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [url] * properties: * url: * type: string * format: uri * description: URL to capture * example: "https://example.com" * format: * type: string * enum: [png, jpeg, webp] * default: png * description: Output image format * width: * type: integer * minimum: 320 * maximum: 3840 * default: 1280 * description: Viewport width in pixels * height: * type: integer * minimum: 200 * maximum: 2160 * default: 800 * description: Viewport height in pixels * fullPage: * type: boolean * default: false * description: Capture full scrollable page instead of viewport only * quality: * type: integer * minimum: 1 * maximum: 100 * default: 80 * description: JPEG/WebP quality (ignored for PNG) * waitForSelector: * type: string * description: CSS selector to wait for before capturing (e.g. "#main-content") * deviceScale: * type: number * minimum: 1 * maximum: 3 * default: 1 * description: Device scale factor (2 = Retina) * delay: * type: integer * minimum: 0 * maximum: 5000 * default: 0 * description: Extra delay in ms after page load before capturing * waitUntil: * type: string * enum: [load, domcontentloaded, networkidle0, networkidle2] * default: domcontentloaded * description: > * Page load event to wait for before capturing. * "domcontentloaded" (default) is fastest for most pages. * Use "networkidle2" for JS-heavy SPAs that load data after initial render. * examples: * simple: * summary: Simple screenshot * value: { "url": "https://example.com" } * hd_jpeg: * summary: HD JPEG * value: { "url": "https://github.com", "format": "jpeg", "width": 1920, "height": 1080, "quality": 90 } * mobile: * summary: Mobile viewport * value: { "url": "https://example.com", "width": 375, "height": 812, "deviceScale": 2 } * responses: * 200: * description: Screenshot image binary * content: * image/png: * schema: { type: string, format: binary } * image/jpeg: * schema: { type: string, format: binary } * image/webp: * schema: { type: string, format: binary } * 400: * description: Invalid request (bad URL, blocked domain, etc.) * content: * application/json: * schema: { $ref: "#/components/schemas/Error" } * 401: * description: Missing API key * content: * application/json: * schema: { $ref: "#/components/schemas/Error" } * 403: * description: Invalid API key * content: * application/json: * schema: { $ref: "#/components/schemas/Error" } * 429: * description: Rate or usage limit exceeded * content: * application/json: * schema: { $ref: "#/components/schemas/Error" } * 503: * description: Service busy (queue full) * content: * application/json: * schema: { $ref: "#/components/schemas/Error" } * 504: * description: Screenshot timed out * content: * application/json: * schema: { $ref: "#/components/schemas/Error" } */ screenshotRouter.post("/", async (req: any, res) => { const { url, format, width, height, fullPage, quality, waitForSelector, deviceScale, delay, waitUntil } = req.body; if (!url || typeof url !== "string") { res.status(400).json({ error: "Missing required parameter: url" }); return; } try { const result = await takeScreenshot({ url, format: format || "png", width: width ? parseInt(width, 10) : undefined, height: height ? parseInt(height, 10) : undefined, fullPage: fullPage === true || fullPage === "true", quality: quality ? parseInt(quality, 10) : undefined, waitForSelector, deviceScale: deviceScale ? parseFloat(deviceScale) : undefined, delay: delay ? parseInt(delay, 10) : undefined, waitUntil: ["load", "domcontentloaded", "networkidle0", "networkidle2"].includes(waitUntil) ? waitUntil : undefined, }); res.setHeader("Content-Type", result.contentType); res.setHeader("Content-Length", result.buffer.length); res.setHeader("Cache-Control", "no-store"); res.send(result.buffer); } catch (err: any) { logger.error({ err: err.message, url }, "Screenshot failed"); if (err.message === "QUEUE_FULL") { res.status(503).json({ error: "Service busy. Try again shortly." }); return; } if (err.message === "SCREENSHOT_TIMEOUT") { res.status(504).json({ error: "Screenshot timed out. The page may be too slow to load." }); return; } if (err.message.includes("blocked") || err.message.includes("not allowed") || err.message.includes("Invalid URL")) { res.status(400).json({ error: err.message }); return; } res.status(500).json({ error: "Screenshot failed", details: err.message }); } });