Framejet Screenshot
透過遠端 MCP 或 REST 取得乾淨的 PNG/JPEG 螢幕截圖,支援目標導向的多步驟導覽。預設移除 Cookie 橫幅和聊天小工具。
託管 MCP 伺服器
npx add-mcp 'https://framejet.dev/mcp'可安裝到 Claude Code、Codex、Cursor 等客戶端
文件
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
| Param | Default | Description |
|---|---|---|
url | — | Required. http/https page to capture. |
X-Api-Key | — | HTTP header. Alternatively use Authorization: Bearer. |
format | png | png or jpeg. |
full_page | false | Capture the entire scroll height. |
width | 1280 | Viewport width, 320–3840. |
height | 800 | Viewport height, still validated with full_page=true; the page content determines the final capture height. |
dpr | 1 | Device pixel ratio, 1–3. See the retina screenshot API guide. |
clean | true | Remove cookie banners, sticky bars, chat widgets. |
delay | 0 | Extra wait in ms after load, max 10000. |
cache | true | Set 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 credits. 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. The website thumbnail guide walks through a directory example. 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. On authenticated requests, X-Framejet-Remaining shows the credits left this month, X-Framejet-Credits-Used what this request cost (0 for a cache hit) and X-Framejet-Policy-Version which plan terms your account is on. Signed URLs carry none of these, because they are public and cached.
Credits and limits
A screenshot costs 1 credit. A goal costs 10 credits per block, screenshot included. A block covers up to five AI steps (decisions, value picks and the final check) and 30,000 tokens; a longer flow takes more blocks. Pass max_credits (10, 20 or 30, default 10) to set the ceiling: that amount is held while the goal runs, the unused part is released, and if the next step would go past it the call stops with 422 goal_budget_exceeded and nothing is charged. It is not part of the cache key. Cache hits and failed captures cost 0 credits.
| Plan | Requests / min | Captures at once | Goal attempts / month | Failed captures / month | Delivery / month |
|---|---|---|---|---|---|
| Free | 10 | 1 | 40 | 100 | 1 GB |
| Starter | 30 | 2 | 1,000 | 500 | 5 GB |
| Pro | 60 | 2 | 5,000 | 2,500 | 15 GB |
| Scale | 120 | 2 | 20,000 | 10,000 | 45 GB |
Cache hits count toward requests per minute and delivery. A failed capture costs no credits but still counts as an attempt. Monthly limits reset on the 1st (UTC); the per-minute ones answer 429 with Retry-After. Free accounts also share a monthly pool of goal capacity. Plans bought before credits kept their original terms.
Errors
JSON { "error": "...", "code": "..." }:
| Status | code | Meaning |
|---|---|---|
| 401 | no_key / invalid_key | Missing or unknown API key. |
| 403 | bad_signature / url_expired | A signed URL was altered, signed with the wrong secret, or is past its expires time. |
| 402 | quota_exceeded | Monthly quota reached. Upgrade on pricing. |
| 429 | rate_limited / concurrency_limited / render_busy | Too many requests per minute, or captures in flight. Wait for the Retry-After seconds and retry. No credits were used. |
| 429 | delivery_limit / goal_attempts_exhausted / failed_attempts_exhausted / free_budget_exhausted | A monthly limit besides credits was reached. It resets on the 1st (UTC); the message says which. |
| 422 | goal_budget_exceeded | The goal would have needed more than max_credits. It stopped before going over; no credits were used. |
| 503 | billing_unavailable | The capture worked but its usage could not be recorded, so the image was not returned and nothing was charged. Retry. |
| 422 | bad_url / blocked_target / target_timeout | The target URL is invalid, private, or didn't load. |
| 422 | target_error | The target answered with HTTP 400 or above, usually bot protection (chatgpt.com returns 403). Framejet doesn't work around it. No quota was used. |
| 422 | bad_action / bad_goal / bad_values / action_failed | A step could not be parsed, or a selector in actions matched nothing. No quota was used. |
| 422 | goal_unreached / values_required | The goal state was not reached, or a field needed text that no supplied value fitted. No quota was used. |
| 500 | capture_failed | Our 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:
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 credits.
Contact
Questions, higher limits, bugs: email founder@framejet.dev.