Screenshot a url
GET /screenshot — bearer auth, JSON responses, billed per successful request.
Screenshot a url
Captures a full-page screenshot of a URL and returns it as a base64-encoded JPEG. Uses the same request parameters as /crawl — same URL, same fetch options, same ai_query — but returns a photographic rendering of the entire scroll height rather than HTML.
When to use it: Use when you need to visually preserve or inspect a page exactly as a browser renders it, including full scroll height, rather than extracting its markup. Useful for archival, visual regression, compliance capture, or any workflow where the rendered appearance matters more than the underlying structure. The flat 30-credit pricing makes costs predictable regardless of proxy tier, geolocation, or AI query usage.
Notes
imagesdefaults totrue(opposite of/crawl), since pages photographed without images show empty boxes where assets belong.- Response payloads grow large: long pages run to several megabytes, and base64 encoding adds ~33% overhead; consider constraining
viewportand reviewing timeouts for tall targets. - Asynchronous jobs return
202— collect completed screenshots from/crawl/{job}underdata.screenshots[0], not the top-levelscreenshotfield used by synchronous200responses.
| Parameter | In | Type | Description |
|---|---|---|---|
| url * | query | string<uri> | The page to photograph. Must be `http` or `https` and must have a host. |
| premium | query | string | Fetch through the premium proxy pool instead of the standard one. Free here, unlike on `/crawl` -- the price is flat -- and worth trying for targets that refuse ordinary requests. Every crawl is proxied either way -- this chooses which pool, not whether there is one. `country` works on both. Sent as text in a query string, so the spellings a person would reach for all work: `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off`, in any case. Anything that is not a yes or a no is refused rather than quietly read as `false`. |
| js | query | string | Render javascript before taking the picture. On by default, because it is what works on most of the web. Free here, unlike on `/crawl`. Accepts the same spellings as `premium`. Off, the page is photographed as the server delivered it: scripts are neither run nor even downloaded, along with anything they would have requested. That is faster, and on most of the modern web it is a picture of an unfinished page -- a client-rendered site photographs blank, and it is still charged for. Turn it off only for a target you know is served finished. |
| images | query | string | Download the images on the page. **On by default here**, which is the opposite of `/crawl` and the only parameter the two endpoints disagree about: what comes back is a picture of the page, and a page photographed without its pictures has empty boxes where they should be. Free either way -- the 2-credit surcharge `/crawl` charges for these does not apply to a flat price. Turning them off is supported for the caller who wants exactly that: image requests are then aborted before they leave the browser while everything else -- HTML, CSS, scripts, XHR -- still loads, so the page lays out normally and photographs faster, with holes in it. On a test page the difference measured 422 KB against 166 KB. Stacks with `adblock`, which blocks a different set of requests. Accepts the same spellings as `premium`. |
| ai_query | query | string | A question to answer about the page, put to a model once the page has loaded and before the picture is taken. Free here, where `/crawl` charges 5 credits for it -- the price is flat. An empty value is the same as asking nothing. The answer comes back under `data`, beside the image rather than instead of it. |
| adblock | query | string | Drop ads and trackers before the page loads, using the EasyList filters. **On by default**, and free either way -- it changes nothing about the price. Ad and tracker payloads dominate a typical retail page, and blocking them cuts what has to be downloaded by around 80% while leaving the page's own assets untouched, so this is the right default for almost every target. What it also removes is anything an ad or analytics script would have written into the document -- a tracking pixel's payload, a `dataLayer` an ad tag populates. Send `adblock=false` if that is what you came for. Accepts the same spellings as `premium`. |
| wait_until | query | load | domcontentloaded | networkidle0 | networkidle2 | When the page counts as loaded. `networkidle0` by default -- it waits for the network to go quiet, which is the slowest of the four and the one most likely to return a finished page. Use `domcontentloaded` or `load` for a page that keeps a connection open (a socket, a poll, an analytics beacon), where waiting for silence means waiting until the crawl times out. |
| viewport | query | string | The window size, written `WIDTHxHEIGHT`. Both sides must be positive. Defaults to `1920x1080`. Required to use `device_scale_factor`, `mobile`, `landscape` or `touch`, which describe a viewport and are meaningless without one. |
| device_scale_factor | query | number | The device pixel ratio, as a retina screen reports it. `3` for a recent phone. Requires `viewport`. |
| mobile | query | string | Tell the page it is on a phone. Requires `viewport`, and accepts the same spellings as `premium`. This changes the window metrics only. A site that decides what to serve from the user agent will still send its desktop page, so pair it with `user_agent`. And a page with no `<meta name="viewport">` tag falls back to a 980px layout viewport whatever width you asked for -- standard browser behaviour rather than the setting being ignored. |
| landscape | query | string | Report the screen as landscape. Requires `viewport`, and accepts the same spellings as `premium`. |
| touch | query | string | Report the screen as touch capable. Requires `viewport`, and accepts the same spellings as `premium`. |
| user_agent | query | string | The user agent to send, replacing the one the crawler would otherwise present. |
| country | query | string | The country to appear to be browsing from, as a two-letter ISO 3166-1 alpha-2 code (`es`, `us`, `de`). Case does not matter. Free, and it works on either proxy pool, so it can be combined with `premium`. Omit it to take whatever exit the pool picks. A country name is not accepted -- `spain` is a `422`, `es` is not. The pool reads the code out of the credentials it is given, so a name would have to be mapped to a code, and that mapping is a list of wrong answers waiting to be picked. Required to use `city`, which narrows a country rather than standing on its own. |
| city | query | string | The city inside that country to appear to be browsing from, as the pool spells it. Case does not matter. **Requires `country`** -- a city on its own is a `422`, because the pool reads it as the segment after a country and not as a location of its own. Free here, where on `/crawl` it doubles the price: this endpoint is flat. Worth knowing that it is the slowest option to serve -- exits in one city are a far smaller pool than exits in a country. Letters, digits, hyphens and underscores only. A multi-word city is written the way the pool writes it -- `new_york`, not `new york` -- and anything carrying a `.` or a `;` is refused: those are the separators the pool parses its own targeting with. Unlike `/crawl`, this endpoint does not echo the city back: there is no price for it to explain. |
| key | query | string | The API key, for callers that cannot set a header. Accepted on the metered routes only -- a key in a url is copied into access logs, proxy logs and `Referer` headers, which is a fair trade for a machine-to-machine endpoint and a bad one for anything else. Ignored when an `Authorization` header is present. |
curl -X GET "https://app.irmu.com/api/screenshot?url=&premium=&js=&images=&ai_query=&adblock=&wait_until=&viewport=&device_scale_factor=&mobile=&landscape=&touch=&user_agent=&country=&city=&key=" \
-H "Authorization: Bearer $IRMU_API_KEY"{
"url": "https://example.com/",
"domain": "example.com",
"screenshot": "/9j/4AAQSkZJRgABAQEAYABgAAD...",
"ai_query": "",
"credits_charged": 30,
"credits_remaining": 970,
"job_id": "0198a9b6-1f4c-7c3e-9a2b-5d8e6f0a1b23_0198a9b6-2a71-7f0d-8c44-9e1b3c5d7f02",
"status": "done",
"data": {
"url": "https://example.com/"
},
"position": null,
"error": null
}const res = await fetch("https://app.irmu.com/api/screenshot", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.IRMU_API_KEY}`,
"Content-Type": "application/json",
}
});
const data = await res.json();import os, requests
res = requests.get(
"https://app.irmu.com/api/screenshot",
headers={"Authorization": f"Bearer {os.environ['IRMU_API_KEY']}"}
)
print(res.json())| Status | Meaning |
|---|---|
| 200 | The picture was taken. `screenshot` holds it: base64, JPEG, full page. Decode it as it stands -- there is no `data:` prefix and no padding to strip: ``` curl -s -H "Authorization: Bearer $KEY" \ "https://irmu.ddev.site/api/screenshot?url=https://example.com" \ | jq -r .screenshot | base64 -d > shot.jpg ``` |
| 202 | Paid for and still going. A tall page, or one that is slow to settle, can outlast the crawler's patience for waiting -- though not its patience for working. Collect it from `/crawl/{job}`, the same endpoint a slow crawl is collected from, and free in the same way. One difference worth knowing: that endpoint answers the crawl envelope, so the image arrives under `data.screenshots[0]` rather than lifted into `screenshot` the way it is here. A running crawl also carries `debug`, and its picture is not the one you paid for: it is a viewport thumbnail off the live tab, showing where the crawl had got to. `screenshot` stays null until the full-page image exists. Null once it finishes, and free either way. |
| 401 | No key, an unknown key, or a caller that is not an organization. |
| 402 | Not enough credits, or the daily limit is reached. Nothing is charged and no attempt is recorded. |
| 422 | A malformed request -- no url, a url that is not `http`/`https`, a city with no country, a `viewport` that is not `WIDTHxHEIGHT`, or a flag that is not a yes or a no. |
| 500 | No picture came back. Either the crawl failed outright, or it finished without producing an image -- which reads as a null `screenshot` and an `error` saying so. **The credits are refunded either way**, so `credits_charged` is `0`. The attempt is still recorded against the domain, so a customer can see that we tried. |
| 503 | The crawl could not be routed through a proxy pool. Nothing is charged. The request was fine and will be fine again once the pool is configured. |
Start building with Irmu today
200 free credits every month, no card required. Every API, every integration, one key.