Vals are Deno functions with a wall-clock limit and no browser. The Val Town docs point at Browserbase for scraping; for screenshots you can skip the browser session and make one GET request.
200 renders a month, no card.
Val Town runs each val in a Deno isolate. You get fetch, environment variables, and a small standard library for blob and SQLite storage. You do not get Chromium, and the web scraping guide in the Val Town docs sends you to Browserbase when a page needs a real browser. That is a reasonable answer for interaction-heavy scraping, but a screenshot is a single image, and a session-based browser API is more machinery than the job needs.
The val below is an HTTP val: it receives a request, calls the screenshot endpoint with the key stored in your Val Town environment variables, and returns the image. The second val caches the result in Val Town blob storage so a shared link does not re-render on every visit.
References: Val Town docs: web scraping (Browserbase examples), Val Town blob storage (std/blob).
Call GET https://app.snap-render.com/v1/screenshot with the target in the url parameter and your key in the X-API-Key header. The response body is the finished image (or PDF when format=pdf). Ads and cookie banners are blocked by default.
# Val Town: Settings > Environment Variables
# Name: SNAPRENDER_API_KEY
# Value: sk_live_your_key_here
export default async function (req: Request): Promise<Response> {
const target = new URL(req.url).searchParams.get('url');
if (!target) {
return Response.json({ error: 'pass ?url=https://example.com' }, { status: 400 });
}
const params = new URLSearchParams({
url: target,
format: 'png',
width: '1280',
height: '800',
cache: 'true',
cache_ttl: '86400',
});
const upstream = await fetch(`https://app.snap-render.com/v1/screenshot?${params}`, {
headers: { 'X-API-Key': Deno.env.get('SNAPRENDER_API_KEY') ?? '' },
});
if (!upstream.ok) {
const body = await upstream.json().catch(() => null);
// 429: RATE_LIMITED or QUOTA_EXCEEDED. 400: INVALID_URL or BLOCKED_URL.
return Response.json({ error: body?.error ?? upstream.statusText }, { status: upstream.status });
}
return new Response(upstream.body, { headers: { 'Content-Type': 'image/png' } });
}
blob.set accepts any BodyInit, so an ArrayBuffer stores as-is. Call blob.delete(key) from a cron val if you want the cache to expire.
import { blob } from "https://esm.town/v/std/blob";
export default async function (req: Request): Promise<Response> {
const target = new URL(req.url).searchParams.get('url');
if (!target) return new Response('pass ?url=', { status: 400 });
const key = `shot:${target}`;
try {
const cached = await blob.get(key);
return new Response(await cached.arrayBuffer(), {
headers: { 'Content-Type': 'image/jpeg', 'X-Source': 'blob' },
});
} catch {
// blob.get throws when the key does not exist; fall through to capture
}
const upstream = await fetch(
`https://app.snap-render.com/v1/screenshot?url=${encodeURIComponent(target)}&format=jpeg&quality=85`,
{ headers: { 'X-API-Key': Deno.env.get('SNAPRENDER_API_KEY') ?? '' } },
);
if (!upstream.ok) return new Response('capture failed', { status: 502 });
const bytes = await upstream.arrayBuffer();
await blob.set(key, bytes);
return new Response(bytes, { headers: { 'Content-Type': 'image/jpeg', 'X-Source': 'api' } });
}
Caching is off by default. Pass cache=true and a cache_ttl in seconds (default 86400, maximum depends on plan). A repeat request for the same URL and parameters inside that window is served from the cache and does not count against your monthly quota. The X-Cache response header reports HIT or MISS, and X-Remaining-Credits shows what is left this month.
Every error body is { "error": { "code", "message", "status" } }.
Full parameter list, POST with raw HTML, signed URLs, and batch jobs are in the API reference. The OpenAPI spec is at /openapi.json.
No. Vals are Deno isolates without a browser binary or subprocess access. The Val Town docs recommend Browserbase for scraping that needs a browser session. For screenshots, one fetch to a screenshot API returns the finished image without a session.
Store it under Environment Variables in your Val Town settings and read it with Deno.env.get. Public vals expose their source, so never paste the key inline.
Yes. Create a cron val that calls the same fetch and writes the result to blob storage or emails it with the std/email module.
No. Requests served from the API cache (cache=true inside cache_ttl) are not billed as renders. Blob storage on the val side adds a second layer so shared links do not touch the API at all.
Yes. The free plan includes 200 renders a month with every feature enabled, and no card is required.
Language quickstarts: Node.js, Python, cURL. Cost comparison: Puppeteer on Lambda vs an API.
Or skip the setup entirely
Everything in this guide is one GET request with SnapRender. No browser to babysit, no timeouts to tune.
200 screenshots a month free. First render in under a minute.
Get a free API key, 200 renders a month, no card.