Irmu Screenshot API

Photographing a page instead of reading it, through the same crawler.

Operations 1

GET /screenshot Screenshot a url #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/irmu-com:irmu-com-screenshot-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

irmu-com-screenshot-api-openapi.yml Raw ↑
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