Framejet Screenshot

Clean PNG/JPEG screenshots via remote MCP or REST, with goal-driven multi-step navigation. Removes cookie banners and chat widgets by default.

Hosted MCP Server

npx add-mcp 'https://framejet.dev/mcp'

Installs into Claude Code, Codex, Cursor and more

Documentation

Screenshot API

One endpoint. Pass a URL, get an image back.

Authentication

Pass your key in the X-Api-Key header or Authorization: Bearer. Query-string keys are not supported. Where you cannot send a header, such as an <img> tag or a spreadsheet, use a signed URL. Get one free on the home page.

How your key works

Enter your email on the home page and confirm the link in your inbox. Your browser generates a new API key. Copy it before leaving; it is never emailed or returned by the server.

Use the same email at checkout. Payment updates your plan without changing an existing API key. If you do not yet have a key, request a verification link from the home page.

Lost your key? Request a recovery link. Confirmation creates a replacement and invalidates the old key immediately.

GET /v1/take

curl -H "X-Api-Key: YOUR_KEY" -o shot.png "https://framejet.dev/v1/take?url=https://example.com"

Parameters

ParamDefaultDescription
url—Required. http/https page to capture.
X-Api-Key—HTTP header. Alternatively use Authorization: Bearer.
formatpngpng or jpeg.
full_pagefalseCapture the entire scroll height.
width1280Viewport width, 320–3840.
height800Viewport height, still validated with full_page=true; the page content determines the final capture height.
dpr1Device pixel ratio, 1–3. See the retina screenshot API guide.
cleantrueRemove cookie banners, sticky bars, chat widgets.
delay0Extra wait in ms after load, max 10000.
cachetrueSet false to force a fresh capture.
actions—Steps to run before capturing, ;-separated, each verb:arg — click:<css>, type:<css>=<text>, waitfor:<css>, wait:<ms>, scroll:<px>. Max 10 steps, 15s of total waiting.
goal—Plain-words description of the state to reach, ending with when to stop. Max 300 characters. See goal mode.
values—| -separated strings a goal step may type. Max 10, 200 characters each. Framejet never invents text.

Maximum 32 million pixels and 4 MB per image. Expensive captures can be rejected with pixel_limit or image_too_large. Each warm renderer permits two simultaneous captures; excess work returns 429.

Goal mode

Use goal mode when the route or controls vary and you cannot name every selector reliably in advance. Describe the final page state and Framejet walks toward it, re-reading the page after every step. If you know the selectors, actions can also click, type and wait for controls that appear after earlier steps. See the screenshot after clicking or searching guide for both approaches.

GET /v1/take
  ?url=https://en.wikipedia.org/
  &goal=Search for the Colosseum article, open it, then open its
        View history page. Stop when the revision list is visible.
  &values=Colosseum

Anything typed comes from values and nothing else — if none of them fits a field, the capture fails rather than guessing. Navigation is decided by a constrained-choice model that picks from the controls actually on the page, so it cannot invent an element.

Use actions instead whenever you know the page. Selector steps are deterministic and reproducible. Goal mode can take several seconds, is capped at 12 steps and 30 seconds, and is not guaranteed to make the same choice twice. Both modes use one screenshot credit when they return a new image. For visual regression testing in particular, use actions — a non-deterministic capture path turns your diffs into noise.

When goal mode reports that it cannot reach the requested state, it returns 422 goal_unreached and costs no quota. A model decides when the goal appears complete, so check the returned image before relying on it for critical workflows.

Signed URLs

A signed URL lets a page, a CMS or a no-code tool load a screenshot directly, for example in <img src>, without exposing your API key. Fetch your key ID and signing secret once:

curl -H "X-Api-Key: YOUR_KEY" https://framejet.dev/v1/signing-key # { "key_id": "…", "signing_secret": "…" }

Build the query string with key_id and any capture parameters, compute HMAC-SHA256 of that exact string with the signing secret, and append &sig= with the hex digest as the last parameter. Sign the bytes you send: do not reorder or re-encode the query afterwards. Keep the signing secret on your server.

import { createHmac } from "node:crypto"; const q = new URLSearchParams({ url: "https://example.com", key_id: KEY_ID, width: "1280" }).toString(); const sig = createHmac("sha256", SIGNING_SECRET).update(q).digest("hex"); const src = `https://framejet.dev/v1/take?${q}&sig=${sig}\`;

import hashlib, hmac from urllib.parse import urlencode q = urlencode({"url": "https://example.com", "key_id": KEY_ID, "width": "1280"}) sig = hmac.new(SIGNING_SECRET.encode(), q.encode(), hashlib.sha256).hexdigest() src = f"https://framejet.dev/v1/take?{q}&sig={sig}"

  • Changing any parameter invalidates the signature, so a signed URL cannot be reused for another page or setting.
  • Add expires (Unix seconds) before signing to make a URL stop working after that time.
  • Signed requests always use the cache, and the response is public for one day, so repeat views do not spend quota. The first capture of each URL uses one screenshot.
  • Replacing your API key changes both values and invalidates every signed URL made with the old ones.

Response

Responses are private/no-store, except signed URLs (see above). Cached images belong to your account and expire after seven days; repeated cache hits do not spend quota.

200 with the image bytes (image/png or image/jpeg). X-Framejet-Cache is HIT or MISS; X-Framejet-Remaining shows screenshots left this month.

Errors

JSON { "error": "...", "code": "..." }:

StatuscodeMeaning
401no_key / invalid_keyMissing or unknown API key.
403bad_signature / url_expiredA signed URL was altered, signed with the wrong secret, or is past its expires time.
402quota_exceededMonthly quota reached. Upgrade on pricing.
422bad_url / blocked_target / target_timeoutThe target URL is invalid, private, or didn't load.
422target_errorThe target answered with HTTP 400 or above, usually bot protection (chatgpt.com returns 403). Framejet doesn't work around it. No quota was used.
422bad_action / bad_goal / bad_values / action_failedA step could not be parsed, or a selector in actions matched nothing. No quota was used.
422goal_unreached / values_requiredThe goal state was not reached, or a field needed text that no supplied value fitted. No quota was used.
500capture_failedOur error — no quota was used.

Examples

JavaScript

const res = await fetch("https://framejet.dev/v1/take?url=https://example.com", { headers: { "X-Api-Key": KEY } }); if (!res.ok) throw new Error(await res.text()); fs.writeFileSync("shot.png", Buffer.from(await res.arrayBuffer()));

Python

r = requests.get("https://framejet.dev/v1/take", params={"url": "https://example.com"}, headers={"X-Api-Key": KEY}) r.raise_for_status() open("shot.png", "wb").write(r.content)

MCP server

Framejet is also a remote Model Context Protocol server, so you can give an AI agent (Claude, Cursor, …) the ability to take screenshots. Add a connector pointing at:

https://framejet.dev/mcp

Authenticate with your API key as a bearer token (Authorization: Bearer YOUR_KEY), or send it as X-Api-Key: YOUR_KEY. It exposes one tool, screenshot, taking the same parameters as above (url, full_page, width, format, clean, goal, values, …) and returning the image inline. Calls draw from the same monthly quota.

Contact

Questions, higher limits, bugs: email founder@framejet.dev.