Irmu
API Reference

Crawl a url

GET /crawl — bearer auth, JSON responses, billed per successful request.

GET/crawl

Crawl a url

Fetch a single URL through a headless browser. The request is charged to the calling organization and tallied against the URL's host for the day. The response returns either the fetched document directly or a JSON envelope containing cost metadata and any AI-generated answer.

When to use it: Use this endpoint for one-off page retrieval when you need rendered content, proxy routing, or AI extraction. For bulk or asynchronous workloads, prefer the job-based flow. Use premium for hostile targets, disable js only when you know the page is server-rendered, and set wait_until to domcontentloaded or load for pages that keep persistent connections open.

Notes

  • The response format branches on content type: text/html for a bare document, application/json when ai_query is present or a screenshot accompanies the page. Do not assume JSON from status alone.
  • js=true is the default; disabling it on a client-rendered site returns an empty page and still charges credits. Verify the target serves meaningful HTML before turning it off.
  • country requires an ISO 3166-1 alpha-2 code (es, not spain); city requires country, doubles the cost, and must match the pool's spelling exactly (new_york, not new york).
ParameterInTypeDescription
url *querystring<uri>The page to crawl. Must be `http` or `https` and must have a host.
premiumquerystringFetch through the premium proxy pool instead of the standard one. Costs more; use it 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`.
jsquerystringRender javascript before reading the page. On by default, because it is what works on most of the web -- a caller who knows their target is static turns it off and pays less. Accepts the same spellings as `premium`. Off, the page comes back as the server delivered it: scripts are neither run nor even downloaded, along with anything they would have requested, which makes this the bluntest lever there is on how long a crawl takes. Only use it where what you came for is in the served html -- on a client-rendered site the page will be empty, and it is still charged for.
imagesquerystringDownload the images on the page. **Off by default**, and the one option here that costs more when you turn it on: 2 credits on top of the fetch, whatever the fetch itself came to. Off, image requests are aborted before they leave the browser and everything else -- HTML, CSS, scripts, XHR -- still loads, so the page behaves normally apart from having no pictures. An `<img>` tag is in the html either way, so its `src` is still there to be read; what you do not get is the file behind it. On a test page the difference measured 422 KB against 166 KB. Turn them on when the bytes are what you came for. Stacks with `adblock`, which blocks a different set of requests. Accepts the same spellings as `premium`.
ai_queryquerystringA question to answer about the page once it has been retrieved. Costs 5 credits on top of the fetch. An empty value is the same as asking nothing and costs nothing extra.
adblockquerystringDrop 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_untilqueryload | domcontentloaded | networkidle0 | networkidle2When 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.
viewportquerystringThe 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_factorquerynumberThe device pixel ratio, as a retina screen reports it. `3` for a recent phone. Requires `viewport`.
mobilequerystringTell 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.
landscapequerystringReport the screen as landscape. Requires `viewport`, and accepts the same spellings as `premium`.
touchquerystringReport the screen as touch capable. Requires `viewport`, and accepts the same spellings as `premium`.
user_agentquerystringThe user agent to send, replacing the one the crawler would otherwise present.
countryquerystringThe 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.
cityquerystringThe 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. **A city doubles what the crawl costs**, surcharges included: exits in one city are a far smaller pool than exits in a country. So `country=de&city=berlin` on the cheapest fetch is 2 credits rather than 1, and on a premium rendered fetch carrying a question it is 60 rather than 30. `credits_charged` is always the figure that was taken. 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. The `city` field of the response echoes the city actually targeted, so it is what to check rather than what was sent.
keyquerystringThe API key, for callers that cannot set a header. Accepted on this route 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.
request.sh
curl -X GET "https://app.irmu.com/api/crawl?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"
200.json
"string"
request.js
const res = await fetch("https://app.irmu.com/api/crawl", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.IRMU_API_KEY}`,
    "Content-Type": "application/json",
  }
});

const data = await res.json();
request.py
import os, requests

res = requests.get(
    "https://app.irmu.com/api/crawl",
    headers={"Authorization": f"Bearer {os.environ['IRMU_API_KEY']}"}
)

print(res.json())
StatusMeaning
200The crawl finished, and the body is the page itself -- `text/html`, with what it cost in headers, because a caller who asked for a url wants the document rather than a document escaped inside a field. A crawl that finished without producing a page -- which is what an `ai_query` does -- answers the JSON envelope instead, with the answer in `data`. Branch on the content type rather than on the status code. One more rule for the same branch: a result that carries both a page and a picture of one comes back as JSON, with the page in `content`, rather than as a bare document that would drop the picture in silence. Nothing produces that pair today -- it is stated so the rule does not have to be guessed at if something does.
202Paid for and running. The runner gives up on waiting long before it gives up on crawling, so a slow page comes back as a job id rather than a result -- collect it from `/crawl/{job}`, which is free. The credits are already spent and the crawl already tallied: a `202` is a crawl that is happening, not one that might. A timeout also shows you where the crawl had got to: `debug` carries the url the browser was on, a viewport screenshot and the page source at that moment. Null when there is nothing to show yet -- a crawl still in the queue has not opened a tab. It costs nothing.
401No key, an unknown key, or a caller that is not an organization -- a signed-in dashboard user reaches this endpoint as a user and is refused here, since a user has no credits to spend.
402The organization's balance will not cover the crawl. Nothing is charged and nothing is recorded.
422The request did not validate.
500The crawler did not deliver. The charge is handed back -- `credits_charged` is `0` and the balance is whole -- and no crawl is counted against the domain, though the attempt is recorded so a customer whose target has started refusing us can see that we tried. Reported as ours rather than as the caller's: the request was valid and paid for, and the crawler is part of this service rather than a third party they chose.
503The crawl could not be routed. Every crawl goes out through a proxy pool, and the one this crawl's tier uses is not available -- a fault at our end rather than anything wrong with the request. Nothing is charged and nothing is recorded. The crawl is refused rather than sent out directly, because connecting without the pool would put our own address in front of the target under your name. Retrying is reasonable; the same request will work once the pool is back.

Start building with Irmu today

200 free credits every month, no card required. Every API, every integration, one key.