API Reference
Run a recipe
POST /crawl — bearer auth, JSON responses, billed per successful request.
POST/crawl
Run a recipe
Runs a user-supplied recipe: an ordered list of crawler blocks that can fill forms, paginate results, and extract structured data. Query parameters control the browser session; the request body defines the automation logic. Costs 30 credits flat regardless of recipe length or proxy options.
When to use it: Use this instead of GET /crawl when you need behavior beyond a single page fetch—form submission, multi-page navigation, or field extraction. Also use it when you want deterministic control over execution order rather than accepting the built recipe the GET generates.
- First block is only for its URL:
blocks[0].details.sourceis read to determine the starting address, but the actual start block is rebuilt from your query parameters.waitUntil,js,viewport, proxy settings, and other browser options in your first block are ignored. - Body fields are strictly validated: Only
blocks,name, andexpectedOutputare permitted in the JSON body. Any other field—including options that belong in the query string—returns422. - Flat pricing, but options still matter:
premium,country,city, and block count do not affect the 30-credit cost, but they still determine routing, proxy pool selection, and whether the target allows the connection.
| Parameter | In | Type | Description |
|---|---|---|---|
| premium | query | boolean | Fetch through the premium proxy pool instead of the standard one. Use it for targets that refuse ordinary requests. Unlike on the `GET`, it does not change the price. |
| js | query | boolean | Render the page's javascript. On by default, and worth leaving on for anything a recipe clicks or types into -- a block that waits for a selector a script would have written will not find it. Does not change the price here. |
| images | query | boolean | Download the images the page references. Off by default. Turn them on for a recipe that screenshots, and leave them off otherwise: an `<img>` tag is in the html whether or not the file behind it was fetched. |
| adblock | query | boolean | Block ads and trackers. On by default. See the `GET` for what it removes and why it is worth leaving on. |
| wait_until | query | load | domcontentloaded | networkidle0 | networkidle2 | When the first page counts as loaded, passed to the browser as-is. Defaults to `networkidle0`. A page that holds a connection open never goes idle, so `domcontentloaded` is the one to reach for there. |
| viewport | query | string | The window size, as `WIDTHxHEIGHT`. Defaults to the crawler's `1920x1080`. Required to use `device_scale_factor`, `mobile`, `landscape` or `touch`. |
| device_scale_factor | query | number | The device pixel ratio. Requires `viewport`. |
| mobile | query | boolean | Report the browser as a mobile device. Requires `viewport`. |
| landscape | query | boolean | Report the viewport as landscape. Requires `viewport`. |
| touch | query | boolean | Report the device as touch-capable. Requires `viewport`. |
| user_agent | query | string | The `User-Agent` to send. Defaults to the crawler's own. |
| country | query | string | The country to appear to be browsing from, as an ISO 3166-1 alpha-2 code. Applies to both pools. Does not change the price here. |
| city | query | string | The city inside that country, as the pool spells it. **Requires `country`.** Letters, digits, hyphens and underscores only. Unlike on the `GET`, a city does not double what the crawl costs: the price here is flat. |
| key | query | string | The API key, for callers that cannot set a header. Ignored when an `Authorization` header is present. |
request.sh
curl -X POST "https://app.irmu.com/api/crawl?premium=&js=&images=&adblock=&wait_until=&viewport=&device_scale_factor=&mobile=&landscape=&touch=&user_agent=&country=&city=&key=" \
-H "Authorization: Bearer $IRMU_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Products, page 2", "expectedOutput": "list", "blocks": [ { "type": "start", "details": { "type": "url", "source": "https://example.com/products" } }, { "id": "rows", "type": "extract", "details": { "selector": ".product .title", "name": "title", "property": "textContent" } }, { "type": "paginate", "details": { "selector": ".pagination .next", "startBlock": "rows" } } ] }'200.json
"string"Guides using this endpoint
- How to Give Claude, ChatGPT and Cursor Live Web Access with an MCP Server
- AI Web Scraping: Extract Data Without Writing Selectors
request.js
const res = await fetch("https://app.irmu.com/api/crawl", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.IRMU_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Products, page 2",
"expectedOutput": "list",
"blocks": [
{
"type": "start",
"details": {
"type": "url",
"source": "https://example.com/products"
}
},
{
"id": "rows",
"type": "extract",
"details": {
"selector": ".product .title",
"name": "title",
"property": "textContent"
}
},
{
"type": "paginate",
"details": {
"selector": ".pagination .next",
"startBlock": "rows"
}
}
]
})
});
const data = await res.json();request.py
import os, requests
res = requests.post(
"https://app.irmu.com/api/crawl",
headers={"Authorization": f"Bearer {os.environ['IRMU_API_KEY']}"},
json={
"name": "Products, page 2",
"expectedOutput": "list",
"blocks": [
{
"type": "start",
"details": {
"type": "url",
"source": "https://example.com/products"
}
},
{
"id": "rows",
"type": "extract",
"details": {
"selector": ".product .title",
"name": "title",
"property": "textContent"
}
},
{
"type": "paginate",
"details": {
"selector": ".pagination .next",
"startBlock": "rows"
}
}
]
}
)
print(res.json())| Status | Meaning |
|---|---|
| 200 | The recipe finished. What comes back depends on what it was: a recipe of one block -- just your start block -- is a request for the page, so the body is the page itself as `text/html` with the cost in headers. Anything longer extracted something, so the body is the JSON envelope with the result 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. |
| 202 | Paid for and running. A recipe that runs longer than the crawler waits comes back as a job id -- collect it from `/crawl/{job}`, which is free. The credits are already spent: a `202` is a crawl that is happening, not one that might. With `debuga=1`, 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. |
| 401 | No key, an unknown key, or a caller that is not an organization -- a signed-in dashboard user is refused here, since a user has no credits to spend. |
| 402 | The organization's balance will not cover the recipe. Nothing is charged and nothing is recorded. |
| 422 | The recipe did not validate. As well as the usual shape errors, this is the answer for a block type the crawler does not run, a `start` block anywhere but first, a `paginate` block naming an id no block carries, and a field in the body that belongs in the query string. |
| 500 | The 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. Note what this does not cover: a recipe that ran to the end and extracted nothing, because every selector missed, is a success. You paid for the browser session and got it. Check `data` rather than the status code for whether the recipe did what you meant. |
| 503 | The crawl could not be routed: the proxy pool this tier uses is not available. Nothing is charged. Retrying is reasonable. |
Start building with Irmu today
200 free credits every month, no card required. Every API, every integration, one key.