The actual request surface
The page is not the only destination.
If your endpoint passes an untrusted URL directly to page.goto(), the browser can reach more than the first hostname. Redirects, images, scripts, frames, stylesheets, and font requests can each trigger another connection. A hostname can also resolve differently between validation and connection.
127.0.0.1 with one regular expression is not enough.IPv6, IPv4-mapped IPv6, link-local addresses, cloud metadata, private DNS answers, mixed public/private answers, redirects, and DNS rebinding all need a connection-time policy.
Put a gate on every outbound connection.
Latchshot first validates the requested target, then runs Chromium through a local outbound proxy. The proxy resolves each HTTP request or HTTPS tunnel, rejects any hostname with a non-public answer, and connects to the validated address rather than handing the hostname back to the operating system.
- Input envelopeAbsolute HTTP(S), no URL credentials, ≤2,048 characters
- Port gateOnly standard web ports 80 and 443
- DNS gateEvery answer must be public; mixed answers fail closed
- Pinned proxyEach request or tunnel connects to a validated IP
- Isolated renderFresh browser context, service workers blocked, downloads disabled
The same proxy path covers the initial document, redirects, and page subresources. It reduces SSRF exposure inside Latchshot; it does not authorize a URL on behalf of your application.
Send one public target from trusted server code.
Keep the Latchshot key outside the browser. Your application authenticates its own user first, decides whether the capture is allowed, and sends only the public target and bounded output options.
curl --fail-with-body \
https://latchshot.fly.dev/v1/render \
-H "Authorization: Bearer $LATCHSHOT_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com",
"kind": "screenshot",
"format": "png",
"width": 1280,
"height": 800,
"waitUntil": "domcontentloaded",
"timeout": 15000
}' \
--output capture.png
On success, the response body is the image. A successful render consumes one monthly success; a rejected target or failed render does not.
Make rejection behavior part of the contract.
Do not return an image of an error page or silently retry a target-policy failure. Latchshot returns structured JSON with an HTTP 400 status before quota consumption.
| Input class | Example | Result |
|---|---|---|
| Local scheme or file | file:///etc/passwd | invalid_target |
| URL credentials | https://user:pass@example.com | invalid_target |
| Loopback or private host | http://127.0.0.1 | private_target |
| Cloud metadata / link-local | http://169.254.169.254 | private_target |
| Mixed DNS answers | One public answer + one private answer | private_target |
| Non-web port | https://example.com:8443 | blocked_port |
| Resolution failure | Hostname has no usable answer | unresolvable_target |
On a narrow screen, scroll the rejection table horizontally.
HTTP/2 400
content-type: application/json
{
"error": {
"code": "private_target",
"message": "local and private network addresses are not allowed"
}
}
Keep authorization in your application.
A guarded renderer protects its own network boundary. It cannot decide whether your user is allowed to capture a competitor, whether a public bearer link contains customer data, or how long your stored image should live.
- Before capture
- Authenticate the caller, authorize the product action, normalize the URL, apply any product-specific domain allowlist, and enforce your own per-user quota.
- During capture
- Use one server-held API key, bound dimensions and timeout, and preserve structured target-policy errors instead of retrying them.
- After capture
- Validate the media type and byte limit, store only if needed, choose retention, and keep the target URL out of public logs and analytics.
A syntactically public URL can still carry a bearer token, signed query string, or private customer identifier. Latchshot intentionally offers no authenticated-page or private-preview contract.
Test the policy, not only the happy-path image.
- Public controlA stable public page returns the expected image type and dimensions.
- Address familiesLoopback, RFC1918, link-local, IPv6 local, and IPv4-mapped IPv6 inputs all fail.
- DNS mixturesA hostname with one public and one private answer fails rather than choosing the public answer.
- Redirect and subresourceA public first URL cannot redirect or load an asset into a private destination.
- Error hygieneOperational detail omits target URLs and API-key-shaped credentials.
- Resource boundsTimeout, page-size, queue, rate, and response-size limits fail explicitly.
Latchshot's public contract covers the hosted behavior. If you operate Playwright yourself, implement equivalent checks at connection time and test them inside the same network where Chromium runs.
Use the guardrail for the job it actually covers.
Use the hosted endpoint
The output starts from one public page.
- Target
- Public HTTP or HTTPS on port 80 or 443
- State
- No cookies, login, customer header, private DNS, or local file
- Output
- One synchronous PNG, JPEG, or PDF owned by your application
Keep a browser in your network
The browser must reach private state.
- Reach
- Internal previews, VPN services, non-standard ports, or local files
- Session
- Cookies, login, custom headers, certificates, or user impersonation
- Workflow
- Clicks, typing, arbitrary scripts, extraction, tracing, or multi-step navigation
This is a defense-in-depth boundary, not a security guarantee or a substitute for application authorization, egress policy, patching, logging, and review.
Run the free browser-local seven-gate check without sending answers or source. Inspect the public risk-ranked report specimen, then use the fixed-scope $149 endpoint safety review for one public-repository JavaScript, TypeScript, or Python screenshot route, including a report, focused patch, regression tests, and residual-risk handoff.
Test the public-only boundary.
Create one card-free key with 100 successful renders each UTC month. Start with a public page you are allowed to capture and keep the rejected-target cases in your integration suite.