# Crawl4AI LLM Scraper — Markdown for AI (`subimpact/crawl4ai-llm-scraper`) Actor

Render any page in a real browser and get LLM-ready Markdown (or HTML/text) with link graph, depth crawling, and Apify Proxy rotation. Pay per usage — no per-page fees.

- **URL**: https://apify.com/subimpact/crawl4ai-llm-scraper.md
- **Developed by:** [subimpact](https://apify.com/subimpact) (community)
- **Categories:** AI, Developer tools
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Crawl4AI LLM Scraper — watch 500 pages for $1

Point it at a site. It renders every page in a real browser, gives you **LLM-ready Markdown**, and then — in watch mode — keeps checking those pages for you at **$0.002 each**. 500 pages watched per dollar. You only pay browser prices for the pages that actually changed.

Powered by the open-source [Crawl4AI](https://crawl4ai.com/) engine on Apify's managed infrastructure.

- **Markdown for AI** — every page rendered in a real browser, converted to clean Markdown with a link graph
- **Watch mode** — $0.002 to check a page that didn't change, browser prices only for the ones that did
- **Structured extraction** — JSON-LD and OpenGraph pulled into typed entities and flat `price` / `author` / `rating` columns
- **AI-ready package** — a JSONL corpus, an `llms.txt` index and a manifest, written straight to the run's key-value store
- **Site intelligence** — broken links, orphan pages, duplicate content and a token estimate, free with any crawl
- **Alerts** — a webhook or Slack message the moment a watched page moves

### Two modes

**Crawl** — the classic scrape. Render pages, follow internal links, get clean Markdown with a link graph.

**Watch** — the cheap one. For each page, a plain HTTP request fetches the HTML, strips the noise (scripts, styles, comments, CSRF nonces, build IDs) and hashes it. If the hash matches last time, the page is reported as unchanged for $0.002 and **the browser is never started**. Only pages whose fingerprint moved get rendered, diffed, and reported as a change.

That is the whole trick: monitoring is cheap because most pages don't change, and you stop paying render prices to find that out.

```
1000 pages, daily, 3% of them change
   watch mode:  970 × $0.002  +  30 × $0.05  =  $3.44 / day
   re-scraping: 1000 × $0.005                =  $5.00 / day, and you still have to diff it yourself
```

### Try it free

Runs with **maxPages ≤ 3**, no watch/alert/incremental/`aiPackage` features, no residential proxy, and ≤ 4 GB memory are **not billed at all**. Free runs still tell you what's there — how many JSON-LD types the page exposes, and whether it changed since you last looked — just with the values withheld:

```json
{
  "structured": { "typesFound": 2, "types": ["Product", "Offer"], "locked": true },
  "changeSignal": { "changedSincePrevious": true, "locked": true }
}
```

Set `estimateOnly: true` on a full-size run to see the exact bill before you commit to it. It crawls everything and charges nothing.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `startUrls` | array | — | Pages to scrape or watch: `[{"url": "https://..."}]` |
| `mode` | string | `crawl` | `crawl` | `watch` |
| `maxDepth` | int | 1 | Levels of internal links to follow (0 = start URLs only) |
| `maxPages` | int | 10 | Hard cap on pages per run (max 1000) |
| `outputFormat` | string | `markdown` | `markdown` | `html` | `text` |
| `includeRawHtml` | bool | false | Also store the raw unrendered HTML |
| `pageLoadTimeoutMs` | int | 30000 | Max render wait per page |
| `renderConcurrency` | int | 5 | Pages rendered in parallel (auto-reduced to fit memory) |
| `extract` | string | `none` | `none` | `auto` | `jsonld` | `og` — structured data per page |
| `aiPackage` | bool | false | Write the JSONL corpus + `llms.txt` + manifest to the run's key-value store |
| `siteIntel` | bool | false | Add the audit block (broken links, orphans, duplicates, tokens) to the summary |
| `watchId` | string | hash of `startUrls` | State store to compare against; share it across runs to track the same site |
| `changeDetection` | string | `fast` | `fast` (HTTP first, render on change) | `rendered` (always render) |
| `changeThreshold` | number | 0.02 | Fraction of lines that must differ to count as a change |
| `skipUnchanged` | bool | false | Incremental crawl: don't re-render pages that haven't changed |
| `alertWebhookUrl` | string | — | HTTPS endpoint to POST changes to; Slack webhooks auto-format |
| `maxChargeUsd` | number | 5.00 | Hard ceiling on what the run may charge |
| `estimateOnly` | bool | false | Run everything, charge nothing, report the estimate |
| `proxyConfiguration` | object | auto | Apify proxy groups |
| `verifyProxyRotation` | bool | false | Debug: report the exit IP of two probe requests |

### Output

One dataset item per page, plus a run summary. The **Changes** dataset view shows just the deltas.

```json
{
  "recordType": "page",
  "url": "https://example.com/pricing",
  "title": "Pricing",
  "markdown": "# Pricing\n...",
  "changeType": "changed",
  "diffRatio": 0.14,
  "charsDelta": 320,
  "previousCrawledAt": "2026-09-03T08:00:00Z",
  "detectedVia": "render",
  "addedLines": ["| Pro | $49/mo |"],
  "removedLines": ["| Pro | $39/mo |"],
  "entityType": "Product",
  "price": 49.0,
  "currency": "USD",
  "structured": { "types": ["Product", "Offer"], "jsonld": [], "og": {}, "canonical": "..." },
  "links": { "internal": ["..."], "external": ["..."] },
  "chars": 12345,
  "depth": 0,
  "crawledAt": "2026-09-04T08:00:00Z"
}
```

`changeType` is `new`, `unchanged`, `changed` or `removed`. Unchanged pages found on the cheap path carry `detectedVia: "http"` and no content — they cost $0.002 and didn't start a browser.

The final item is the summary:

```json
{
  "recordType": "summary", "mode": "watch", "checked": 500, "unchanged": 486,
  "changed": 12, "new": 2, "removed": 0, "estimatedChargeUsd": 1.582
}
```

### Structured extraction

Set `extract` and every page also comes back as typed data instead of just prose.

| `extract` | What it reads |
|---|---|
| `none` (default) | Nothing is published — you still see *which* types a page exposes, with the values locked |
| `jsonld` | `<script type="application/ld+json">` blocks, including `@graph` wrappers and arrays |
| `og` | `og:` / `twitter:` meta tags and the canonical URL; `og:type` stands in for the entity type |
| `auto` | Both |

```json
"structured": {
  "types": ["Product", "Offer"],
  "jsonld": [ { "@type": "Product", "name": "Widget", "offers": { "price": "19.99" } } ],
  "og": { "title": "Widget", "image": "https://..." },
  "twitter": { "card": "summary_large_image" },
  "canonical": "https://shop.example/widget"
}
```

The parts a spreadsheet can use are also lifted to the top level, so the **Structured data** dataset view is a readable table: `entityType`, `price`, `currency`, `publishedAt`, `author`, `rating`. Prices are parsed from JSON-LD `offers` (including `priceSpecification` and `AggregateOffer.lowPrice`), falling back to `product:price:amount`. A price we cannot parse confidently — a range like `"from 10 to 20"` — is passed through as a string rather than guessed at, because a silently wrong number in a price column is worse than an unparsed one.

**Billing follows the value**: `structured-extract` is charged per page **where at least one type was actually found**. A page with no structured data costs nothing extra, so pointing this at a whole site doesn't bill you for its empty pages.

> **Changed in v0.3.** In v0.2 the parsed JSON-LD was attached to every paid page whether you asked for it or not. It is now behind `extract` — leave it at `none` and you get the locked count, exactly like a free run. Set `extract: "auto"` to get the values back.

### AI-ready package

`aiPackage: true` writes three files to the run's default key-value store when the crawl ends:

| Key | What it is |
|---|---|
| `ai-package.jsonl` | The corpus — one JSON object per page: `{url, title, markdown, structured, crawledAt}` |
| `llms.txt` | The link index in [llms.txt](https://llmstxt.org/) format: one `- [Title](url)` per page, with the OpenGraph description where there is one |
| `manifest.json` | `{actor, version, runId, pageCount, generatedAt, schema: {fields: [...]}}` — what the bundle holds and how to read it |

Point a RAG pipeline at the run, read `ai-package.jsonl`, done — no dataset paging, no reshaping. Pages with no content (a watch-mode page that didn't change, so was never rendered) are listed in `llms.txt` but left out of the corpus; `manifest.linkIndexCount` and `manifest.pageCount` tell you both numbers. A corpus over 8 MB is truncated rather than failing the write, and says so in `manifest.truncated` and in the log.

$0.100, once per run, charged only after the bundle is actually written. A run that scraped nothing writes nothing and charges nothing.

### Site intelligence

`siteIntel: true` adds an audit block to the run summary. It is computed entirely from what the run already fetched — no extra requests, no extra charge:

```json
{
  "recordType": "summary",
  "brokenLinks": [ { "url": "https://site/old", "status": 404, "detectedVia": "render" } ],
  "brokenLinkCount": 3,
  "orphanCount": 7,
  "orphanPages": ["https://site/legacy/page"],
  "duplicateCount": 4,
  "duplicateGroups": [ { "hash": "9f2c...", "urls": ["https://site/a", "https://site/b"], "count": 2 } ],
  "tokenEstimate": 412300,
  "topPages": [ { "url": "https://site/docs", "title": "Docs", "chars": 48210 } ]
}
```

- **brokenLinks** — pages the crawl reached that answered 4xx/5xx, or failed to load at all. A 404 page still renders fine, which is exactly how a broken internal link surfaces here.
- **orphanCount** — URLs this watch knows about that nothing crawled in this run links to. Start URLs are entry points, never orphans. This is only meaningful on a **completed** crawl with history: a run cut short by `maxPages` has only seen part of the link graph, and the log says so.
- **duplicateCount** — pages whose normalized content hashes identically to another page. One original per group is not counted as a duplicate.
- **tokenEstimate** — total characters ÷ 4. What it would cost an LLM to read this site.
- **topPages** — the ten largest pages by character count.

### Alerts

Set `alertWebhookUrl` to an HTTPS endpoint and every detected change is POSTed to it:

```json
{ "url": "...", "changeType": "changed", "diffRatio": 0.14, "title": "Pricing", "changedAt": "..." }
```

A Slack incoming webhook (`https://hooks.slack.com/services/...`) is detected automatically and sent as Slack blocks instead. Delivery is billed only on a 2xx; timeouts and errors are logged and never fail the run. The URL must be `https` and must resolve to a public address — loopback, private and link-local targets are rejected, and redirects are not followed.

`estimateOnly` runs do **not** send alerts — a dry run shouldn't fill your channel — but they do price the deliveries they would have made, so the estimate stays honest.

### Pricing

Pay per event. Apify compute is billed separately as usual.

| Event | Price | When |
|---|---|---|
| `actor-start` | $0.010 | Once per run. Not charged on free runs. |
| `page-scraped` | $0.005 | Per page rendered. **First 3 per run are free.** |
| `page-monitored` | $0.002 | Per page checked in watch mode with no material change. |
| `change-detected` | $0.050 | Per page with a material content change, or a page that disappeared. |
| `structured-extract` | $0.010 | Per page where `extract` found at least one type. Pages with no structured data are free. |
| `alert-delivered` | $0.020 | Per successful (2xx) webhook delivery. |
| `ai-package` | $0.100 | Once per run, after the bundle is written to the key-value store. |

`maxChargeUsd` is enforced locally, in-run: once the ceiling is hit, nothing further is billed and the summary says so. Charges are also de-duplicated per (URL, event) and persisted, so an Apify migration mid-run cannot double-bill you.

### Use cases

- **Competitor and pricing monitoring** — watch a catalogue daily, get a webhook when a price moves
- **Docs / policy change tracking** — know when a vendor edits their terms, with the added and removed lines
- **Incremental RAG ingestion** — `skipUnchanged: true` re-embeds only what actually changed
- **Drop-in RAG corpus** — `aiPackage: true` hands your pipeline a JSONL corpus and an `llms.txt` index instead of a dataset to reshape
- **Product / catalogue extraction** — `extract: "auto"` turns a shop crawl into `entityType` / `price` / `currency` / `rating` columns
- **Content audits** — `siteIntel: true` for broken links, orphan pages, duplicate content and the token cost of a site, from a crawl you were running anyway
- RAG / knowledge-base ingestion, LLM training-data prep, AI-agent research pipelines

### How change detection works

1. Plain HTTP GET, no browser.
2. Normalize: drop `<script>`, `<style>`, comments, `nonce`/CSRF/`data-build-*` attributes, collapse whitespace. This is what stops a rotating CSRF token from billing you for a change every hour.
3. SHA-256 the result and compare to the stored fingerprint. Match → unchanged, $0.002, done.
4. Mismatch (or a non-200, or `changeDetection: "rendered"`) → render in the browser and compare line by line. A change counts only if the diff ratio clears `changeThreshold`.
5. A URL known from a previous run but not seen this time is reported as `removed` — but **only if the crawl actually completed**. A run truncated by `maxPages` or wrecked by failures never reports its unvisited tail as deleted.

Fingerprints live in a named key-value store, `c4ai-watch-<watchId>`, one record per URL, holding the hash, character count, title, a body snapshot for diffing (up to 200 KB), and the page's internal links. Pages above 200 KB are tracked by hash only: still reported as changed, without the line-level diff. Records not seen for 90 days are dropped.

### Proxy and IP rotation

Requests are routed through Apify Proxy using a small pool of sticky sessions, rotated per request. Sessions are bounded rather than unlimited, because Playwright binds a proxy when a browser **context** is created, not per navigation — an unbounded set of proxy URLs would either be ignored or spawn an unbounded set of contexts.

**Verify it rather than trust it.** Set `verifyProxyRotation: true` and the run fetches an IP echo endpoint twice on two different sessions and writes both exit IPs to the dataset:

```json
{ "recordType": "proxyRotationCheck", "firstIp": "1.2.3.4", "secondIp": "5.6.7.8", "rotating": true }
```

If `rotating` is `false`, both requests left through the same IP and rotation on this Crawl4AI version is per-run, not per-request — plan around that instead of assuming.

### Notes

- Crawl4AI is MIT-licensed open source; this actor is a managed wrapper around it.
- Structured extraction reads Crawl4AI's `result.html` — the post-JavaScript DOM with `<script>` tags intact — and never `result.cleaned_html`, which strips exactly the `ld+json` blocks the parser needs. So JSON-LD injected by client-side JavaScript is picked up too.
- For sites with aggressive anti-bot protection, use the residential proxy group in `proxyConfiguration`.
- Crawl runs also record fingerprints, so you can crawl a site once and flip to `mode: "watch"` afterwards with no re-scrape.

# Actor input Schema

## `startUrls` (type: `array`):

List of pages to scrape or watch. Each entry: {"url": "https://..."}

## `mode` (type: `string`):

Crawl = scrape every page. Watch = check each page for changes and only render the ones that moved.

## `maxDepth` (type: `integer`):

How many levels of internal links to follow (0 = only the start URLs)

## `maxPages` (type: `integer`):

Hard cap on the number of pages processed per run

## `outputFormat` (type: `string`):

Format of the page content stored in the dataset

## `includeRawHtml` (type: `boolean`):

Also store the raw (unrendered) HTML for each page

## `pageLoadTimeoutMs` (type: `integer`):

Max time to wait for a page to render

## `renderConcurrency` (type: `integer`):

How many pages to render in parallel. Automatically reduced to fit the run's memory limit.

## `extract` (type: `string`):

Pull structured data out of every page. JSON-LD reads <script type="application/ld+json"> blocks; OpenGraph reads og:/twitter: meta tags plus the canonical URL. Auto does both. Billed at $0.010 per page where a type is actually found — pages with no structured data cost nothing.

## `aiPackage` (type: `boolean`):

Write ai-package.jsonl (one JSON record per page), llms.txt (the link index) and manifest.json to this run's key-value store, ready for a RAG pipeline. $0.100 once per run. Not available on free runs.

## `siteIntel` (type: `boolean`):

Add an audit block to the run summary: broken links, orphan pages, duplicate content, token estimate and the largest pages. Computed from what the run already fetched — no extra crawling, no extra charge.

## `watchId` (type: `string`):

Name of the state store that remembers previous page fingerprints. Runs sharing a watch ID compare against each other. Defaults to a hash of the start URLs.

## `changeDetection` (type: `string`):

Fast = plain HTTP fetch first, render only on a mismatch (much cheaper). Rendered = always render before comparing (needed for JS-built pages).

## `changeThreshold` (type: `number`):

How different a page must be to count as changed, 0-1. 0.02 = 2% of lines differ.

## `skipUnchanged` (type: `boolean`):

In crawl mode, skip rendering pages whose content has not changed since the last run with the same watch ID.

## `alertWebhookUrl` (type: `string`):

HTTPS URL to POST to when a change is detected. Slack incoming webhooks are auto-formatted as Slack messages.

## `maxChargeUsd` (type: `number`):

Hard ceiling on what this run may charge. Once reached, no further events are billed.

## `estimateOnly` (type: `boolean`):

Run the full crawl and report what it would cost, without charging anything.

## `proxyConfiguration` (type: `object`):

Apify proxy for IP rotation. Leave default for automatic proxy.

## `verifyProxyRotation` (type: `boolean`):

Fetch an IP echo endpoint twice on two different proxy sessions and report both exit IPs to the dataset. Use this to confirm rotation is actually happening.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://crawlee.dev"
    }
  ],
  "mode": "crawl",
  "maxDepth": 1,
  "maxPages": 10,
  "outputFormat": "markdown",
  "includeRawHtml": false,
  "pageLoadTimeoutMs": 30000,
  "renderConcurrency": 5,
  "extract": "none",
  "aiPackage": false,
  "siteIntel": false,
  "changeDetection": "fast",
  "changeThreshold": 0.02,
  "skipUnchanged": false,
  "maxChargeUsd": 5,
  "estimateOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "verifyProxyRotation": false
}
```

# Actor output Schema

## `recordType` (type: `string`):

page | summary | proxyRotationCheck.

## `url` (type: `string`):

Scraped page URL.

## `title` (type: `string`):

Page title.

## `markdown` (type: `string`):

LLM-ready markdown (when outputFormat=markdown).

## `html` (type: `string`):

Rendered HTML (when outputFormat=html).

## `text` (type: `string`):

Plain text (when outputFormat=text).

## `rawHtml` (type: `string`):

Raw unrendered HTML (when includeRawHtml=true).

## `format` (type: `string`):

Which content format this item carries.

## `links` (type: `string`):

Internal and external links found on the page (JSON).

## `depth` (type: `string`):

Crawl depth of this page.

## `chars` (type: `string`):

Character count of the stored content.

## `linkCount` (type: `string`):

Total links found on the page.

## `crawledAt` (type: `string`):

ISO timestamp of the crawl.

## `changeType` (type: `string`):

new | unchanged | changed | removed.

## `diffRatio` (type: `string`):

Fraction of lines that differ from the previous version (0-1).

## `charsDelta` (type: `string`):

Character count change since the previous version.

## `previousCrawledAt` (type: `string`):

When this page was last fingerprinted.

## `detectedVia` (type: `string`):

http (cheap check) | render (browser) | absence (page gone).

## `addedLines` (type: `string`):

Lines added since the previous version (capped at 200).

## `removedLines` (type: `string`):

Lines removed since the previous version (capped at 200).

## `structured` (type: `string`):

types, jsonld, og, twitter and canonical for the page. Counts only (locked) on free runs or when extract=none.

## `entityType` (type: `string`):

Primary schema.org type of the page, e.g. Product or Article.

## `price` (type: `string`):

Offer price found in JSON-LD or product: meta tags.

## `currency` (type: `string`):

ISO currency code for the price.

## `publishedAt` (type: `string`):

datePublished from JSON-LD, or article:published\_time.

## `author` (type: `string`):

Author name from JSON-LD or the author meta tag.

## `rating` (type: `string`):

aggregateRating.ratingValue from JSON-LD.

## `aiPackage` (type: `string`):

On the summary item: what the AI-ready bundle contains and which key-value store keys hold it.

## `brokenLinks` (type: `string`):

On the summary item (siteIntel): pages that answered 4xx/5xx or failed to load.

## `orphanCount` (type: `string`):

On the summary item (siteIntel): known pages linked from nothing that was crawled.

## `duplicateCount` (type: `string`):

On the summary item (siteIntel): pages whose content is identical to another page.

## `tokenEstimate` (type: `string`):

On the summary item (siteIntel): rough LLM token count of everything crawled.

## `topPages` (type: `string`):

On the summary item (siteIntel): the ten largest pages by character count.

## `changeSignal` (type: `string`):

Locked change indicator shown on free runs.

## `estimatedChargeUsd` (type: `string`):

On the summary item: what this run cost, or would have cost in estimate mode.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        {
            "url": "https://crawlee.dev"
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("subimpact/crawl4ai-llm-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": [{ "url": "https://crawlee.dev" }],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("subimpact/crawl4ai-llm-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    {
      "url": "https://crawlee.dev"
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call subimpact/crawl4ai-llm-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,subimpact/crawl4ai-llm-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/9ofgvROubzFkujMB9/builds/TWfeX7fqMSZgYkIb2/openapi.json
