Site-Shot

Tutorial ·Sep 2, 2026 ·10 min read

How to Give a LangChain Agent Website Screenshots

Your agent can read a page's HTML. It still cannot see the page. Rendered layout, the chart in the hero image, the cookie wall covering the offer, the ad that only loads in Germany — none of that survives a text scrape, and all of it is what someone means when they ask an agent "does this page look right?"

The fix is a tool that returns a picture. LangChain's own tools documentation uses exactly this as its canonical example of a tool returning multimodal content — a function literally named capture_screenshot. What the docs show is the easy half. The half that will cost you an afternoon is that the documented example returns a URL, most screenshot APIs return bytes, and whether the image reaches the model at all depends on which provider you are pointed at.

This is the working version, with the parts that break called out.

The short answer

Define a @tool that calls a screenshot API and returns a list of content blocks — one text block naming what the picture is, one image block carrying base64. On Claude models that image reaches the model directly from the tool result. On OpenAI models it does not, and you have to route the image in as a user message instead. Budget for the image being resent on every subsequent turn, and treat whatever is in the screenshot as untrusted input.

The tool, in full

Install the framework and the Site-Shot Python SDK — zero dependencies, standard library only:

pip install langchain langchain-anthropic site-shot

The whole tool is fifteen lines:

from langchain.tools import tool
from site_shot import SiteShot

client = SiteShot()  # reads SITESHOT_API_KEY from the environment


@tool
def capture_screenshot(url: str) -> list[dict]:
    """Capture a full-page screenshot of a public web page and return the image.

    Use this when the answer depends on how the page looks rather than what its
    HTML says: layout, rendered charts, images, banners, or visual regressions.
    Takes a single public URL. Does not work on pages behind a login.
    """
    png = client.capture_base64(
        url, full_size=True, no_ads=True, no_cookie_popup=True
    )
    return [
        {"type": "text", "text": f"Full-page screenshot of {url}:"},
        {"type": "image", "base64": png, "mime_type": "image/png"},
    ]

Three things in there are load-bearing and easy to get wrong.

The type hints are not decoration. LangChain's docs are explicit: "Type hints are required as they define the tool's input schema." Drop the url: str annotation and the tool will not build.

The docstring is the tool description the model actually reads when deciding whether to call this instead of fetching HTML. The version above tells it when the tool applies and when it doesn't. A one-line docstring gets you an agent that screenshots things it should have read.

mime_type is mandatory here. LangChain's content-block reference marks it "Required for base64 data". Omit it and you get an unhelpful failure well downstream of the line that caused it.

Wire it to a model and run it:

from langchain.chat_models import init_chat_model

model = init_chat_model("claude-opus-5", model_provider="anthropic")
agent = model.bind_tools([capture_screenshot])

Why not the URL form the docs show

LangChain's example returns {"type": "image", "url": "https://example.com/page.png"}. That is the cheapest possible path — the provider fetches the image itself and nothing large passes through your process.

It also assumes the screenshot already lives at a public URL. Site-Shot's API returns the image as the response body, and its JSON mode returns a data URL rather than a hosted link, so there is no URL to hand over. You have two honest options: base64 the bytes into the message, as above, or upload the capture to your own storage first and pass that link.

Base64 is the right default. Upload-first is worth it when the same screenshot will be referenced across many turns, because a stored image costs one URL in the transcript forever, while a base64 image costs its full size on every request for the rest of the conversation.

That last point is not a rounding error, so it gets its own section below.

The provider split nobody documents

This is the part that turns a working prototype into a broken one when you switch models.

Claude models accept images inside a tool result. Anthropic's tool-use documentation states that a tool_result block's content "can use the text, image, document, or search_result types", and ships a worked example of a tool result carrying a base64 image. The tool above works as written.

OpenAI models do not. Images in tool responses are not supported, and that holds for the Responses API too. The tool returns, the request fails, and the error points at message roles rather than at your tool.

The workaround is to stop trying to deliver the picture through the tool. Have the tool return a plain-text reference, and append the image as a user message on the next turn:

@tool
def capture_screenshot(url: str) -> str:
    """Capture a full-page screenshot of a public web page. Returns a reference."""
    captures[url] = client.capture_base64(url, full_size=True, no_ads=True)
    return f"Captured {url}. The screenshot is attached in the next message."
# after executing the tool call, before the next model request:
messages.append({
    "role": "user",
    "content": [
        {"type": "image", "base64": captures[url], "mime_type": "image/png"},
        {"type": "text", "text": "This is the screenshot you requested."},
    ],
})

Uglier, and it works everywhere. If your agent needs to run against both providers, write the second form and skip the branch.

Keeping the context from exploding

An agent conversation resends its whole history on every request. A base64 screenshot is therefore not paid for once — it is paid for on every turn that follows it, for as long as it stays in the transcript.

A full-page capture of a long marketing page can run into megabytes. Three of those early in a ten-turn conversation is a bill you will notice. Four levers, cheapest first:

  • no_ads=True and no_cookie_popup=True — already in the snippet above. A cookie wall covering the fold is simultaneously a useless image and a full-price one.
  • Capture the viewport, not the page. Drop full_size and pass width=1280, height=800 when the question is about the top of the page, which it usually is.
  • format="jpeg" for photographic pages. Meaningfully smaller than PNG; a poor choice for screenshots of text and UI, where PNG stays sharper.
  • max_height to cap a full-page capture that would otherwise run to twenty thousand pixels.

If the agent only needs to check something rather than keep it, drop the image from the history once it has been described. LangChain's own guidance for long-running agents is to prefer references over base64 blocks.

Geotargeting, and failing loudly

Pass a two-letter ISO 3166-1 country code and the render routes through a real IP address in that country, with the language, time zone and geolocation defaults that belong to it:

png = client.capture_base64(
    "https://example.com/", country="DE", strict_country=True, no_ads=True
)

Full country names are not valid values — it is country="DE", never country="Germany".

strict_country is the parameter to care about in an agent. Without it, a country whose proxy pool is momentarily exhausted falls back to a US render and returns a perfectly normal screenshot of the wrong thing. An agent cannot tell that happened. It will describe the US page in confident detail and you will have no signal that anything went wrong. With strict_country=True the call fails instead: the API answers "error": "country_unavailable", and the Python SDK surfaces that as a CountryUnavailableError you can catch. Catch it inside the tool and hand the failure back to the model as text — that is the failure you want, because a silently-wrong geo screenshot is undetectable downstream.

Geotargeting comes with any paid plan. The current country list lives on the countries page and follows live proxy capacity, so it changes.

Treat the screenshot as untrusted input

A screenshot is a picture of a page you do not control, handed to a model that follows instructions.

Anthropic's documentation is direct about this: tool results "often carry content from sources outside your control: web pages, inbound email, user uploads, third-party APIs", and an attacker who can influence that content "may embed instructions that try to redirect Claude". A page can render text sized for the model rather than the reader. The screenshot delivers it faithfully.

Two habits cover most of it. Keep the image inside the tool_result block rather than promoting it into a system prompt — the boundary is what lets the model treat it as data. And do not let an agent that captures arbitrary URLs also hold credentials or the ability to send things outward in the same loop; the screenshot is the injection vector and the outbound tool is the payload.

None of this is specific to screenshots. It is specific to any tool that returns content from the open web, which is most of the interesting ones.

Where the honest limits are

Pages behind a login are out of scope. The API captures public URLs. If your agent needs to sign in, step through a flow and then look, that is a browser-driving job, not a screenshot-API job — we wrote about where that line falls.

There is no LangChain integration package. This is a tool you write, in the fifteen lines above, not a pip install that registers itself. That is a fair trade for most people — the tool is short and you control the docstring, which is the part that decides whether your agent calls it correctly.

The model has to support images at all. LangChain's docs put it plainly: "The model must support the modalities you return. Check your model's capabilities before returning images."

Screenshots are not a text extractor. If you want the words, ask for the rendered HTML — cheaper, exact, and no vision tokens. Screenshots earn their cost when the appearance is the question.

The free browser tool has no API. site-shot.com captures any page with no signup, which is the fastest way to check what a capture of your target actually looks like before you write code. It is a browser tool, not a free API tier; the API needs a key, and plans start at $5/mo for 2,000 screenshots.

FAQ

Can a LangChain tool return an image? Yes. A LangChain tool returns image content by returning a list of content blocks containing an image block — either {"type": "image", "url": ...} for an already-hosted image, or {"type": "image", "base64": ..., "mime_type": "image/png"} for raw bytes, where mime_type is required for base64 data. Whether the model accepts it depends on the provider.

Why does my LangChain screenshot tool fail on OpenAI models but work on Claude? Because OpenAI does not support images in tool responses, including through the Responses API, while Anthropic's tool-use specification allows a tool_result block to contain image content blocks. To support both, have the tool return a text reference and append the image as a separate user message instead of returning it from the tool.

How much context does a screenshot cost an agent? A base64 image is re-sent with the entire conversation history on every subsequent request, so a full-page capture is charged again on each turn it remains in the transcript. Capture the viewport instead of the full page, remove ads and cookie banners, and drop the image from history once the agent has described it.

Can a LangChain agent take a screenshot from another country? Yes. Pass a two-letter ISO 3166-1 country code such as country="DE" to route the render through a real IP address in that country, and add strict_country=True so an exhausted country pool raises CountryUnavailableError in your own code instead of silently falling back to a US render. Geotargeting is included with any paid Site-Shot plan.

Is a screenshot from an agent tool a security risk? It can be. A screenshot is content from a page you do not control, delivered to a model that follows instructions, so it is a route for indirect prompt injection. Keep the image inside the tool result rather than a system prompt, and avoid giving one agent both the ability to capture arbitrary URLs and the ability to send data outward.

The free Site-Shot browser tool captures any public page with no signup — try your target URL before you write the tool. For the API, plans and pricing start at $5/mo for 2,000 screenshots.

← All articles