Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Irmu Com Screenshot API
version: 1.0.0
description: 'Operations tagged Screenshot across 2 of this provider''s published API definitions: app.irmu.com.yaml, irmu-com-openapi.yaml. Each path carries the servers of the definition it was published in.'
servers:
- url: https://app.irmu.com/api
description: Production
tags:
- name: Screenshot
description: Photographing a page instead of reading it, through the same crawler.
paths:
/screenshot:
get:
tags:
- Screenshot
summary: Screenshot a url
operationId: screenshot
description: 'Takes a full-page screenshot of a url and returns it as base64 JPEG.
The same request as `/crawl` -- the same url, the same fetch options, the
same `ai_query` -- answered with a picture of the page instead of its
html. The crawler loads the page exactly as it would for a crawl, then
photographs the whole of it: not the viewport, the entire scroll height.
**30 credits, flat.** Unlike `/crawl`, none of the options change that.
`premium`, `js`, `country` and `city` still choose how the page is fetched
and where the crawl exits, `images` and `ai_query` still cost nothing
extra, and a city does not double the bill. What this costs is a browser
laying out, scrolling and encoding a whole page rather than a page''s worth
of bytes.
Two things to know before the first call. **`images` is on by default
here**, against off on `/crawl`, because a page photographed without its
pictures has empty boxes where they should be. And the response can be
large: a long page runs to several megabytes and base64 adds about a third
on top, so a tall target is worth a `viewport` and a thought about
timeouts.
Always JSON, never `text/html`. There is nothing to branch on here.
The key goes in the `Authorization` header, or in `?key=` as on `/crawl`.'
security:
- apiKeyHeader: []
- apiKeyQuery: []
parameters:
- name: url
in: query
required: true
description: The page to photograph. Must be `http` or `https` and must have a host.
schema:
type: string
format: uri
maxLength: 2048
example: https://example.com/products?page=2
- name: premium
in: query
required: false
description: '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`.
'
schema:
type: string
default: 'false'
example: 'false'
- name: js
in: query
required: false
description: '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.
'
schema:
type: string
default: 'true'
example: 'true'
- name: images
in: query
required: false
description: '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`.
'
schema:
type: string
default: 'true'
example: 'true'
- name: ai_query
in: query
required: false
description: '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.
'
schema:
type: string
maxLength: 1000
example: What is the price?
- name: adblock
in: query
required: false
description: '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`.
'
schema:
type: string
default: 'true'
example: 'false'
- name: wait_until
in: query
required: false
description: '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.
'
schema:
type: string
enum:
- load
- domcontentloaded
- networkidle0
- networkidle2
default: networkidle0
example: networkidle0
- name: viewport
in: query
required: false
description: '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.
'
schema:
type: string
pattern: ^[1-9][0-9]{0,4}x[1-9][0-9]{0,4}$
default: 1920x1080
example: 390x844
- name: device_scale_factor
in: query
required: false
description: 'The device pixel ratio, as a retina screen reports it. `3` for a
recent phone. Requires `viewport`.
'
schema:
type: number
minimum: 0.1
maximum: 5
example: 3
- name: mobile
in: query
required: false
description: '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.
'
schema:
type: string
default: 'false'
example: 'true'
- name: landscape
in: query
required: false
description: 'Report the screen as landscape. Requires `viewport`, and accepts the
same spellings as `premium`.
'
schema:
type: string
default: 'false'
example: 'false'
- name: touch
in: query
required: false
description: 'Report the screen as touch capable. Requires `viewport`, and accepts
the same spellings as `premium`.
'
schema:
type: string
default: 'false'
example: 'true'
- name: user_agent
in: query
required: false
description: 'The user agent to send, replacing the one the crawler would otherwise
present.
'
schema:
type: string
maxLength: 512
example: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1
- name: country
in: query
required: false
description: '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.
'
schema:
type: string
minLength: 2
maxLength: 2
pattern: ^[A-Za-z]{2}$
example: es
- name: city
in: query
required: false
description: '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.
'
schema:
type: string
pattern: ^[A-Za-z][A-Za-z0-9_-]{0,63}$
example: berlin
- name: key
in: query
required: false
description: '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.
'
schema:
type: string
responses:
'200':
description: "The picture was taken. `screenshot` holds it: base64, JPEG, full page.\n\nDecode it as it stands -- there is no `data:` prefix and no padding to\nstrip:\n\n```\ncurl -s -H \"Authorization: Bearer $KEY\" \\\n \"https://irmu.ddev.site/api/screenshot?url=https://example.com\" \\\n | jq -r .screenshot | base64 -d > shot.jpg\n```\n"
content:
application/json:
schema:
$ref: '#/components/schemas/ScreenshotResult'
example:
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
'202':
description: '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.
With `debuga=1`, 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.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ScreenshotResult'
example:
url: https://example.com/
domain: example.com
screenshot: null
ai_query: ''
credits_charged: 30
credits_remaining: 970
job_id: 0198a9b6-1f4c-7c3e-9a2b-5d8e6f0a1b23_0198a9b6-2a71-7f0d-8c44-9e1b3c5d7f02
status: running
data: null
position: 2
error: null
debug:
url: https://example.com/
screenshot: /9j/4AAQSkZJRgABAQ...
html: <!doctype html>...
'401':
description: 'No key, an unknown key, or a caller that is not an organization.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
message: This endpoint is reached with an organization API key.
'402':
description: 'Not enough credits, or the daily limit is reached. Nothing is charged
and no attempt is recorded.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: '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.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
'500':
description: '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.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ScreenshotResult'
example:
url: https://example.com/
domain: example.com
screenshot: null
ai_query: ''
credits_charged: 0
credits_remaining: 1000
job_id: 0198a9b6-1f4c-7c3e-9a2b-5d8e6f0a1b23_0198a9b6-2a71-7f0d-8c44-9e1b3c5d7f02
status: done
data:
url: https://example.com/
position: null
error: The crawler took no screenshot of that page. Nothing was charged.
'503':
description: '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.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
message: The crawler cannot route this crawl right now. Nothing was charged.
servers:
- url: https://app.irmu.com/api
description: Production
components:
schemas:
CrawlSnapshot:
type: object
description: 'Where a crawl had got to, for one that is still running or failed. Only
sent when the request carries `debuga=1`.
The crawler stops waiting long before it stops working, and when it stops
waiting it looks at the tab: the url the browser is on, a picture of what
is on screen, and the page source as it stands. That is what this is --
a diagnosis for a request that timed out, not a result. Nothing is
charged for it.
Null on anything that is not still running. A finished or failed crawl has
no tab left to look at, and a crawl still in the queue has not opened one
yet.
Two things to know when reading one. The image is the **viewport**, not
the whole page, so it is not the picture `/screenshot` sells -- it is a
thumbnail at quality 60, meant to be looked at rather than kept. And
either artefact may be missing: they are captured independently, because a
navigation can destroy one without touching the other, so half a snapshot
is ordinary. The missing half reads as null rather than going absent.
A blank image beside a near-empty `html` is not a broken capture -- it
means the crawl stalled *inside* a navigation, with the old document
already replaced. That is itself the answer.
'
required:
- url
- screenshot
- html
properties:
url:
type:
- string
- 'null'
format: uri
description: 'The url the tab was on, which a redirect may have moved away from the
one you asked for.
'
example: https://shop.example/p/1
screenshot:
type:
- string
- 'null'
format: byte
description: 'A base64 JPEG of the viewport, with no `data:` prefix -- decode it as
it stands. Null when the capture raced a navigation and lost.
'
example: /9j/4AAQSkZJRgABAQ...
html:
type:
- string
- 'null'
description: 'The page source at that moment, half-loaded and all. Null when the
capture raced a navigation and lost.
'
example: <!doctype html>...
Error:
type: object
required:
- message
properties:
message:
type: string
ValidationError:
type: object
required:
- message
- errors
properties:
message:
type: string
errors:
type: object
additionalProperties:
type: array
items:
type: string
ScreenshotResult:
type: object
description: 'What `/screenshot` answers. `screenshot` is null while a crawl is still
running and when it finished without producing an image; `status` and
`error` are what tell those two apart.
Standalone rather than the crawl envelope, like `/chatgpt`''s: there is no
`content` here, because a recipe that ends in a screenshot is never a
document recipe and so never comes back as a page.
'
required:
- url
- domain
- screenshot
- ai_query
- credits_charged
- credits_remaining
- job_id
- status
- data
- position
- error
- debug
properties:
url:
type: string
format: uri
description: The page that was photographed, as it was asked for.
example: https://example.com/
domain:
type: string
description: 'The host the crawl was tallied against for the day, lowercased.
'
example: example.com
screenshot:
type:
- string
- 'null'
format: byte
description: 'The picture: a full-page JPEG, base64 encoded, with no `data:` prefix.
Full page means the entire scroll height rather than the viewport, so
a long page produces a tall image and a large field -- several
megabytes is ordinary, and base64 adds about a third on top.
Null when there is no picture, which is either a crawl still running
or one that produced none. `status` says which, and `error` says why.
'
example: /9j/4AAQSkZJRgABAQEAYABgAAD...
ai_query:
type: string
description: 'The question that was asked, echoed back. Empty when none was. Here to
explain `data` rather than the bill: it costs nothing on this endpoint.
'
example: ''
credits_charged:
type: integer
description: '30 for a screenshot that was taken or is still being taken, 0 for one
that produced nothing -- those are refunded. Flat: none of the options
move it.
'
example: 30
credits_remaining:
type: integer
description: The organization's balance afterwards.
example: 970
job_id:
type: string
description: 'What this ran under. Collect a `202` from `/crawl/{job}` with it.
'
example: JOB_ID
status:
type: string
enum:
- queued
- running
- done
- failed
description: '`queued` and `running` mean paid for and not finished -- poll it.
`done` and `failed` are final.
'
example: done
data:
type:
- object
- 'null'
additionalProperties: true
description: 'Whatever else the crawl collected -- the answer to an `ai_query` when
one was asked, and the page''s own url and title. Null when it
collected nothing.
The image is **not** in here. It is lifted into `screenshot` and
removed from this object rather than repeated, because sending several
megabytes twice is not a rounding error.
'
example:
url: https://example.com/
position:
type:
- integer
- 'null'
description: Where it sits in the crawler's queue while `queued`.
example: null
error:
type:
- string
- 'null'
description: Why no picture came back. Null otherwise.
example: null
debug:
description: 'Where the crawl had got to, when the crawler stopped waiting and
looked. Only sent when the request carries `debuga=1` -- a screenshot
and the page source run to a megabyte -- and null otherwise.
The picture in here is **not** the one you paid for. `screenshot`
above is the deliverable, a full-page image; this is a viewport
thumbnail off the live tab, and it never stands in for the other. A
pending response carries this one and not that one.
'
oneOf:
- $ref: '#/components/schemas/CrawlSnapshot'
- type: 'null'
securitySchemes:
apiKeyHeader:
type: http
scheme: bearer
description: '`Authorization: Bearer <key>`. The usual way in.
'
apiKeyQuery:
type: apiKey
in: query
name: key
description: '`?key=<key>`. Accepted on the crawl route only, and only when no
`Authorization` header was sent.
'
x-refined-from:
- app.irmu.com.yaml
- irmu-com-openapi.yaml