# Grabber: every page

> Grabber is a web page API for AI agents: one GET request turns a public URL into clean Markdown, HTML, plain text, links or metadata. $0.001 a page, $0.005 with JavaScript rendering, prepaid with no subscription, and errors are free.

---

# Grabber API docs

> Get started with the Grabber API: one GET request turns any URL into Markdown, HTML, text or links. Quickstart, code samples, parameters and errors.

Grabber loads one web page and returns it as markdown, HTML, text, links, or
the raw body. It is made for AI agents and scripts.

There is one endpoint, `GET /fetch`. Every call needs
`Authorization: Bearer <key>`. Create a key at
https://dashboard.grabber.sh/keys. Keys start with `gsh_`. A key goes in the
header only: `?key=` is refused like any other unknown parameter.

## Quickstart

1. **Create an account.** [Sign up](https://dashboard.grabber.sh/signup) and verify your email. You get $1 of free credit, enough for 1,000 pages, with no card.
2. **Create an API key.** Open [API keys](https://dashboard.grabber.sh/keys) and create one. It starts with `gsh_` and is shown once, so store it as `GRABBER_API_KEY`.
3. **Make a request.** Call `GET /fetch` with the key in the `Authorization` header and the page in `url`. The examples below are ready to run.

## Your first request

Ask for Markdown and get the JSON envelope back: what was loaded, from where, and the content.

### cURL

```bash
curl -G https://api.grabber.sh/fetch \
  -H "Authorization: Bearer $GRABBER_API_KEY" \
  --data-urlencode "url=https://example.com/start" \
  -d "formats=markdown"
```

### Python

```python
import os
import requests

response = requests.get(
    "https://api.grabber.sh/fetch",
    params={"url": "https://example.com/start", "formats": "markdown"},
    headers={"Authorization": f"Bearer {os.environ['GRABBER_API_KEY']}"},
    timeout=130,
)
response.raise_for_status()
print(response.text)
```

### Node.js

```javascript
const params = new URLSearchParams({ url: 'https://example.com/start', formats: 'markdown' })

const response = await fetch(`https://api.grabber.sh/fetch?${params}`, {
  headers: { Authorization: `Bearer ${process.env.GRABBER_API_KEY}` },
  signal: AbortSignal.timeout(130_000),
})
if (!response.ok) throw new Error(`Grabber answered ${response.status}`)
console.log(await response.text())
```

### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	query := url.Values{"url": {"https://example.com/start"}, "formats": {"markdown"}}
	req, _ := http.NewRequest("GET", "https://api.grabber.sh/fetch?"+query.Encode(), nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("GRABBER_API_KEY"))

	client := &http.Client{Timeout: 130 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status)
	fmt.Println(string(body))
}
```

### PHP

```php
<?php
$query = http_build_query(['url' => 'https://example.com/start', 'formats' => 'markdown']);

$ch = curl_init("https://api.grabber.sh/fetch?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GRABBER_API_KEY')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 130,
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body;
```

### Response

```json
{
  "status": 200,
  "message": "OK",
  "data": {
    "url": "https://example.com/start",
    "fetch": {
      "loadedUrl": "https://example.com/final",
      "loadedTime": "2026-10-21T16:36:49.840Z",
      "httpStatusCode": 200,
      "contentLengthBytes": 9,
      "contentType": "text/html",
      "encoding": "utf8"
    },
    "markdown": "ok"
  }
}
```

## Just the content

Add `unwrap=true` and the body is the content itself, with its own content type: Markdown here. That is often what you want to hand to a model.

```bash
curl -G https://api.grabber.sh/fetch \
  -H "Authorization: Bearer $GRABBER_API_KEY" \
  --data-urlencode "url=https://example.com/" \
  -d "formats=markdown" \
  -d "unwrap=true"
```

With no envelope, the response headers are the only place the spend is reported: `X-Grabber-Cost` says what the request cost and `X-Grabber-Balance` what is left.

## Reference

- [Authentication](https://grabber.sh/docs/authentication): API keys, the Authorization header and 401s.
- [Parameters](https://grabber.sh/docs/parameters): Every query parameter and every response field.
- [Errors](https://grabber.sh/docs/errors): Each error code, what it means and what to do.
- [Pricing and limits](https://grabber.sh/docs/pricing-and-limits): What is charged, credit, and request limits.
- [MCP server](https://grabber.sh/mcp): Give Claude, Cursor and other agents the same API as a tool.
- [OpenAPI](https://grabber.sh/openapi.json): The full spec, for code generators and agents.

Source: https://grabber.sh/docs

---

# Authentication

> Every Grabber API request needs an API key in the Authorization header. How to create a key, send it, keep it safe and read a 401 response.

An API key from https://dashboard.grabber.sh/keys. It starts with `gsh_`.
The header is the only place it is accepted.

Send it as a Bearer token on every request:

```http
Authorization: Bearer gsh_your_api_key
```

## Send the key

Each example reads the key from the GRABBER_API_KEY environment variable.

### cURL

```bash
curl -G https://api.grabber.sh/fetch \
  -H "Authorization: Bearer $GRABBER_API_KEY" \
  --data-urlencode "url=https://example.com/" \
  -d "formats=markdown" \
  -d "unwrap=true"
```

### Python

```python
import os
import requests

response = requests.get(
    "https://api.grabber.sh/fetch",
    params={"url": "https://example.com/", "formats": "markdown", "unwrap": "true"},
    headers={"Authorization": f"Bearer {os.environ['GRABBER_API_KEY']}"},
    timeout=130,
)
response.raise_for_status()
print(response.text)
```

### Node.js

```javascript
const params = new URLSearchParams({ url: 'https://example.com/', formats: 'markdown', unwrap: 'true' })

const response = await fetch(`https://api.grabber.sh/fetch?${params}`, {
  headers: { Authorization: `Bearer ${process.env.GRABBER_API_KEY}` },
  signal: AbortSignal.timeout(130_000),
})
if (!response.ok) throw new Error(`Grabber answered ${response.status}`)
console.log(await response.text())
```

### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	query := url.Values{"url": {"https://example.com/"}, "formats": {"markdown"}, "unwrap": {"true"}}
	req, _ := http.NewRequest("GET", "https://api.grabber.sh/fetch?"+query.Encode(), nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("GRABBER_API_KEY"))

	client := &http.Client{Timeout: 130 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status)
	fmt.Println(string(body))
}
```

### PHP

```php
<?php
$query = http_build_query(['url' => 'https://example.com/', 'formats' => 'markdown', 'unwrap' => 'true']);

$ch = curl_init("https://api.grabber.sh/fetch?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GRABBER_API_KEY')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 130,
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body;
```

## Only in the header

A key in the address, like `?key=gsh_…`, is refused like any other unknown parameter: the answer is `400 invalid_query` and the key is not used. Keys in URLs end up in logs and browser history, so Grabber never reads them there.

## When the key is wrong

The key is missing, malformed, unknown, or deleted. The answer is `401` with `WWW-Authenticate: Bearer`, and nothing is charged.

```json
{
  "status": 401,
  "message": "Unauthorized",
  "detail": "invalid_api_key"
}
```

## Keep your key safe

- Keep the key on a server or in your agent's environment, never in code that runs in a browser or in a public repository.
- Create one key per app or agent. The dashboard shows usage and cost per key, and you can delete one without touching the others.
- If a key leaks, delete it in the [dashboard](https://dashboard.grabber.sh/keys). It stops working at once, and the next request with it gets a `401`.

Source: https://grabber.sh/docs/authentication

---

# Request parameters

> Every parameter of GET /fetch: url, formats, unwrap, includeMetadata and js_render, plus the response envelope and metadata fields you get back.

Loads `url` and returns the selected representations. Only `GET` is
accepted: any other method is `405` with `Allow: GET`.

An omitted `formats` list is markdown for HTML and PDF, text for other
textual types, and raw for images and other binary types.

`unwrap=true` returns the first requested format this content can
produce, as the response body, with that format's content type:
`text/markdown`, `text/html`, `text/plain`, a JSON array of links, or
the target's own media type for `raw`. `includeMetadata` is ignored
when `unwrap` is true. If none of the requested formats can be
produced, the answer is `502 format_unavailable`.

The `url` must be `http` or `https`, must not include a username or
password, and must be at most 8192 characters. Localhost, `.local`
names, private and loopback addresses, and cloud metadata hosts are
`400 blocked_url`. A redirect to one of those hosts is
`502 blocked_url`. A fetched body over 8 MiB is `502 body_too_large`.

Unknown parameters are refused, so a typo is reported instead of
ignored. If the address you want to fetch has its own query string,
percent-encode the whole `url` value, or its `&` will be read as the
start of another parameter.

## Query parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `url` | string, up to 8,192 characters | Yes | — | The address to fetch. Percent-encode it. Give it exactly once. |
| `formats` | list of `markdown`, `html`, `text`, `links`, `raw` | No | — | What to return, in the order you want it: a comma-separated list (`formats=markdown,links`). Repeating the parameter (`formats=markdown&formats=links`) works too. An empty item is refused. |
| `unwrap` | boolean | No | `false` | `true` returns the first format as the response body, instead of the JSON envelope. Only `true` or `false`, given at most once. |
| `includeMetadata` | boolean | No | `false` | `true` adds `data.metadata` (title, description, language, Open Graph tags, JSON-LD, links, phones, emails). Ignored when `unwrap` is true. Only `true` or `false`, given at most once. |
| `js_render` | boolean | No | `false` | `true` runs the page in a headless browser first, so content that depends on JavaScript is included. It costs $0.005 instead of $0.001 and can take up to 120 seconds. Only `true` or `false`, given at most once. |

## Formats

Ask for several in one request: they are all built from the same load, and it costs the same as one.

| Format | With unwrap=true | What you get |
| --- | --- | --- |
| `markdown` | `text/markdown` | The readable content of the page as Markdown: headings, lists, tables, links and code, without scripts and styles. |
| `html` | `text/html` | The page HTML. With `js_render=true`, the HTML after scripts ran. |
| `text` | `text/plain` | The readable content as plain text, with no markup at all. |
| `links` | JSON array | Every link on the page as an absolute URL. |
| `raw` | The page's own type | The body exactly as the server sent it. Binary bodies are base64 in the envelope. |

## Response fields

Without unwrap, the answer is a JSON envelope: { status, message, data }. These are the fields of data.

| Field | Type | Description |
| --- | --- | --- |
| `data.url` | string | The URL that was requested. |
| `data.fetch.loadedUrl` | string | The URL after redirects. |
| `data.fetch.loadedTime` | string | When the page was loaded, in ISO 8601. |
| `data.fetch.httpStatusCode` | integer | Status returned by the target, including 4xx and 5xx. |
| `data.fetch.contentLengthBytes` | integer | Size of the body Grabber received, in bytes. |
| `data.fetch.contentType` | string | Media type of the page, as its server sent it. |
| `data.fetch.encoding` | `utf8` or `base64` | `utf8` for text, `base64` when `raw` holds a binary body. |
| `data.metadata` | object | Title, description, Open Graph tags and more. Present with `includeMetadata=true`. |
| `data.markdown` | string | The page as Markdown. Present when `formats` includes `markdown`. |
| `data.html` | string | The page HTML. Present when `formats` includes `html`. |
| `data.text` | string | The page as plain text. Present when `formats` includes `text`. |
| `data.links` | list of strings | Absolute URLs of the links on the page. Present when `formats` includes `links`. |
| `data.raw` | string | The original body. UTF-8 text when `fetch.encoding` is `utf8`, and base64 when `fetch.encoding` is `base64`. |

## Metadata fields

With includeMetadata=true, data.metadata has every one of these fields. A field the page does not have is null or an empty list.

| Field | Type | Description |
| --- | --- | --- |
| `canonicalUrl` | string or null | The canonical URL the page declares. |
| `title` | string or null | The page title. |
| `description` | string or null | The meta description. |
| `author` | string or null | The author meta tag. |
| `keywords` | string or null | The keywords meta tag. |
| `languageCode` | string or null | The language of the page, such as `en`. |
| `openGraph` | list of objects | Every Open Graph tag, as `{ property, content }`. |
| `jsonLd` | list of objects | Every JSON-LD block on the page, parsed. |
| `links` | list of strings | Absolute URLs of the links on the page. |
| `phones` | list of strings | Phone numbers found on the page. |
| `emails` | list of strings | Email addresses found on the page. |

## Response examples

### JSON envelope

```json
{
  "status": 200,
  "message": "OK",
  "data": {
    "url": "https://example.com/start",
    "fetch": {
      "loadedUrl": "https://example.com/final",
      "loadedTime": "2026-10-21T16:36:49.840Z",
      "httpStatusCode": 200,
      "contentLengthBytes": 9,
      "contentType": "text/html",
      "encoding": "utf8"
    },
    "markdown": "ok"
  }
}
```

### Rendered HTML and metadata

```json
{
  "status": 200,
  "message": "OK",
  "data": {
    "url": "https://example.com/start",
    "fetch": {
      "loadedUrl": "https://example.com/start",
      "loadedTime": "2026-10-21T16:36:49.840Z",
      "httpStatusCode": 200,
      "contentLengthBytes": 48,
      "contentType": "text/html",
      "encoding": "utf8"
    },
    "metadata": {
      "canonicalUrl": null,
      "title": "Hi",
      "description": null,
      "author": null,
      "keywords": null,
      "languageCode": "en",
      "openGraph": [],
      "jsonLd": [],
      "links": [],
      "phones": [],
      "emails": []
    },
    "html": "<p>rendered</p>",
    "markdown": "rendered"
  }
}
```

### Unwrapped link list

```json
[
  "https://example.com/docs"
]
```

Source: https://grabber.sh/docs/parameters

---

# Errors

> Every error the Grabber API returns, with its HTTP status, its stable detail code and what to do next. Errors are never charged.

Every error has the shape `{ status, message, detail }`, plus a `hint` when
there is something the caller can do about it. `detail` is a stable code to
branch on; `message` is the HTTP reason phrase. Nothing is ever cached:
every answer has `Cache-Control: no-store`.

You pay only for a page that was loaded, so no error is ever charged. A page whose own server answered 404 or 500 is not an error: it was loaded, so the answer is `200` and it is charged. Its status is in `data.fetch.httpStatusCode`.

## Error codes

| Status | detail | Meaning |
| --- | --- | --- |
| `400` | `invalid_query` | The query is wrong, and `hint` says how. |
| `400` | `invalid_url` | The url is not `http` or `https`, has a username or password, or is too long. |
| `400` | `blocked_url` | The url points at a host that cannot be fetched. |
| `401` | `invalid_api_key` | The key is missing, malformed, unknown, or deleted. |
| `402` | `insufficient_credits` | The balance is below the price of this request. Nothing was charged and nothing was fetched. |
| `404` | `not_found` | The path is not `/fetch`. |
| `405` | `method_not_allowed` | The method is not `GET`. |
| `429` | `rate_limited` | More than 60 requests in this minute for this key. Wait `Retry-After` seconds. |
| `500` | `internal_error` | Something failed on Grabber's side. Nothing was charged. Try again; if it keeps happening, it is not caused by your request. |
| `502` | `fetch_failed` | The page could not be loaded. |
| `502` | `blocked_url` | The page redirected to a host that cannot be fetched. |
| `502` | `body_too_large` | The page is over 8 MiB. |
| `502` | `format_unavailable` | `unwrap=true`, and none of the requested formats can be produced for this content. |
| `502` | `upstream_unavailable` | Grabber could not get an answer from its fetching service. |

## Examples

### No url, or more than one

```json
{
  "status": 400,
  "message": "Bad Request",
  "detail": "invalid_query",
  "hint": "Give exactly one address to fetch, for example ?url=https://example.com/"
}
```

### A parameter that does not exist

```json
{
  "status": 400,
  "message": "Bad Request",
  "detail": "invalid_query",
  "hint": "Unknown parameter \"key\". If it is part of the address to fetch, percent-encode the whole url value."
}
```

### A format that does not exist

```json
{
  "status": 400,
  "message": "Bad Request",
  "detail": "invalid_query",
  "hint": "One of the values in formats is not supported. Use markdown, html, text, links or raw."
}
```

### No credit

```json
{
  "status": 402,
  "message": "Payment Required",
  "detail": "insufficient_credits",
  "hint": "Add credits at https://dashboard.grabber.sh/billing"
}
```

### Rate limited

```json
{
  "status": 429,
  "message": "Too Many Requests",
  "detail": "rate_limited"
}
```

## What to do

- **400**: the request itself is wrong, so sending it again gives the same answer. Read `hint`, fix the query or the URL, then retry.
- **401**: check that the key is sent as `Authorization: Bearer gsh_…` and still exists in the dashboard.
- **402**: add credit at the address in `hint`, or turn on auto top-up so the balance never runs out.
- **429**: wait the number of seconds in `Retry-After`, then retry. The limit is 60 requests per minute per key.
- **500** and **502 `upstream_unavailable`**: the problem is on Grabber's side. Retry with a growing delay, such as 1, 2, then 4 seconds.
- **502 `fetch_failed`**: the page did not load. Check the URL in a browser; if it opens there, try again later.
- **502 `format_unavailable`**: with `unwrap=true`, add a format this content can produce, such as `raw`, after the one you want.

## Response headers

| Header | Meaning |
| --- | --- |
| `X-Grabber-Cost` | What this request cost, in US dollars: `0.001`, `0.005`, or `0` when nothing was charged. |
| `X-Grabber-Balance` | What is left of your credit after this request, in US dollars. |
| `Cache-Control` | Always `no-store`. Answers are never cached. |
| `WWW-Authenticate` | Always `Bearer`. |
| `Allow` | Always `GET`. |
| `Retry-After` | Seconds until the next minute begins. |

Source: https://grabber.sh/docs/errors

---

# Pricing and limits

> What a Grabber API request costs, when you are charged, how prepaid credit works, and the rate, size and time limits of every request.

## What it costs

Grabber is prepaid. A fetch costs **$0.001**, or **$0.005** with
`js_render=true`. You pay only for a page that was loaded: any error
costs nothing. A page whose own server answered 404 or 500 is still a loaded
page, so it is a `200` and it is charged. Read `data.fetch.httpStatusCode`.

Every answer that got as far as billing carries `X-Grabber-Cost` (what this
request cost, in USD) and `X-Grabber-Balance` (what is left). With
`unwrap=true` the body is the content itself with no envelope, so these headers
are the only place the spend is reported.

## Prices

| Item | Price |
| --- | --- |
| A page | **$0.001** |
| A page with `js_render=true` | **$0.005** |
| Any error | $0 |
| Credit when you sign up | $1, about 1,000 pages |
| Smallest top-up | $5 |
| Credit lifetime | 365 days from when you add it |

## Credit

Grabber is prepaid, with no plan or subscription. Add credit in the [dashboard](https://dashboard.grabber.sh/billing), from $5; the presets are $10, $25, $50, $100. You pay by card through Stripe, and Grabber pays Stripe's fee, so every dollar you pay becomes a dollar of credit.

With auto top-up, your saved card is charged when the balance falls below a level you choose, $5 unless you change it, and $25 of credit is added. Credit is valid for 365 days after you add it.

## Limits

60 requests per minute per key. Every request with a valid key counts,
including one refused for a bad query or for lack of credit. Over the limit the
answer is `429` with a `Retry-After` header.

A rendered fetch (`js_render=true`) can take up to 120 seconds. Use a client
timeout of at least 130 seconds.

| Limit | Value |
| --- | --- |
| Requests per minute, per key | 60 |
| Length of `url` | 8,192 characters |
| Size of the page | 8 MiB |
| Time for a rendered page | Up to 120 seconds |
| Suggested client timeout | 130 seconds |
| Methods | `GET` only |

Source: https://grabber.sh/docs/pricing-and-limits

---

# Give your agents the web with one MCP server

> Add Grabber's remote MCP server to Claude, Cursor, VS Code or Windsurf, and your agent can read any web page as Markdown. $0.001 per page.

Add https://mcp.grabber.sh to your MCP client and your agent gets a fetch_page tool that reads any public page as clean Markdown, paid from the same prepaid credit as the API.

- Remote: nothing to install
- Claude, Cursor, VS Code, Windsurf
- Same key and credit as the API

[Get API key](https://dashboard.grabber.sh/signup) · [API docs](https://grabber.sh/docs)

## Set it up in a minute

Paste the config for your client and replace the key with your own.

- Create an API key in the dashboard: dashboard.grabber.sh/keys
- Add Grabber to your client with the config on the right
- Restart the client and ask it to read a page

### Cursor

`~/.cursor/mcp.json`

```json
{
  "mcpServers": {
    "grabber": {
      "url": "https://mcp.grabber.sh",
      "headers": {
        "Authorization": "Bearer gsh_your_api_key"
      }
    }
  }
}
```

### Claude Code

`Terminal`

```bash
claude mcp add --transport http grabber https://mcp.grabber.sh \
  --header "Authorization: Bearer gsh_your_api_key"
```

### VS Code

`.vscode/mcp.json`

```json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "grabber-key",
      "description": "Grabber API key",
      "password": true
    }
  ],
  "servers": {
    "grabber": {
      "type": "http",
      "url": "https://mcp.grabber.sh",
      "headers": {
        "Authorization": "Bearer ${input:grabber-key}"
      }
    }
  }
}
```

### Windsurf

`~/.codeium/windsurf/mcp_config.json`

```json
{
  "mcpServers": {
    "grabber": {
      "serverUrl": "https://mcp.grabber.sh",
      "headers": {
        "Authorization": "Bearer gsh_your_api_key"
      }
    }
  }
}
```

### Claude Desktop

`claude_desktop_config.json`

```json
{
  "mcpServers": {
    "grabber": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://mcp.grabber.sh",
        "--header",
        "Authorization:${GRABBER_AUTH}"
      ],
      "env": {
        "GRABBER_AUTH": "Bearer gsh_your_api_key"
      }
    }
  }
}
```

## Server details

| Setting | Value |
| --- | --- |
| URL | `https://mcp.grabber.sh` |
| Transport | Streamable HTTP |
| Authentication | `Authorization: Bearer gsh_…`, the same key as the API |
| Tools | `fetch_page`, `check_balance` |
| Pricing | Same as the API: $0.001 a page, $0.005 rendered, errors free |

## Tools

Two tools, so the agent always knows which one to call.

| Tool | What it does | Arguments | Cost |
| --- | --- | --- | --- |
| `fetch_page` | Loads one public web page and returns its content, as Markdown unless you ask for another format. | `url` (required): The http or https address to load.<br>`format`: `markdown`, `html`, `text`, `links`. Default `markdown`.<br>`js_render`: Run the page in a headless browser first. Default `false`.<br>`max_chars`: Cut the content after this many characters, at most 100,000. | $0.001, or $0.005 with `js_render`. Errors are free. |
| `check_balance` | Returns the credit left on the account of the key, in US dollars. | None | Free |

## What agents do with it

### Code against current docs

Point Cursor or Claude Code at a library's docs and it writes code for the version you use, not the one in its training data.

### Research with sources

Ask for a summary of a page, a comparison of several, or a check of a claim against the original text.

### Read single-page apps

The agent can set js_render when a page is built by JavaScript, so dashboards and app pages come through.

### Keep the context small

Markdown is a fraction of the size of the HTML, and max_chars caps every page at 100,000 characters or less.

## Try these prompts

- "Read https://example.com/pricing and put the plans in a table."
- "Fetch the docs page for the library we use and update our code to its current API."
- "Read these three articles and tell me where they disagree."
- "How much Grabber credit do I have left?"

## MCP questions

### Do I need to install anything?

No. The server is remote, at mcp.grabber.sh. Clients that support remote servers connect to it directly. Claude Desktop connects through mcp-remote, a small bridge that npx runs for you.

### What does the MCP server cost?

The same as the API, from the same prepaid credit: fetch_page costs $0.001 a page, or $0.005 with js_render. check_balance is free, and so is any request that fails.

### Why is a page capped at 100,000 characters?

So one long page cannot fill the context window of your agent. Set max_chars lower to save more room. The price is the same whatever the cap.

### Is my API key safe in the config file?

The key stays in a file on your computer and is only sent to mcp.grabber.sh, over HTTPS. Use a separate key for each client, so you can see its usage and delete it on its own.

### Which clients work?

Any client that can connect to a remote MCP server over Streamable HTTP and send a header, such as Cursor, Claude Code, VS Code and Windsurf. Claude Desktop works through mcp-remote.

## Add Grabber to your agent

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/mcp

---

# Connect Your Agents To Every Web Page

> One API call turns any URL into clean Markdown for your AI agents. $0.001 per page, no subscription, and failed requests are free.

One API call turns any URL into clean Markdown. Better prices, no subscriptions, no charge for failed requests.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

## Works with

Claude, Cursor, VS Code, Windsurf, LangChain, LlamaIndex, CrewAI, n8n.

## Grabber in numbers

- **120M+** Pages fetched a month
- **5K+** Developers
- **99.9%** API uptime, last 90 days
- **640 ms** Median response time

## Fast, clean web access for your agents

Grabber does one job: it loads a page and hands your model the part worth reading.

### Clean context for models

Pages come back as Markdown, without scripts, styles or markup. Your model reads the content instead of the HTML around it, and every call costs fewer tokens.

### Pay only for loaded pages

A page costs $0.001. Timeouts, blocked addresses and sites that cannot be reached cost nothing, so a bad URL never eats your credit.

### JavaScript rendering on demand

Add js_render=true and the page runs in a headless browser first, so single-page apps come through. $0.005 per rendered page, only when you ask.

## Featured formats

Ask for one format or several in the same request. Each is a page of its own if you want the details.

### Markdown

`formats=markdown`

Headings, lists and links kept as Markdown, with scripts, styles and tags removed. The default for HTML and PDF.

Pages a week: 18.0M · Median time: 650 ms · 4-week growth: +10%

[More about Markdown](https://grabber.sh/url-to-markdown)

### Text

`formats=text`

Plain text with no markup at all, for search indexes, classifiers and embeddings.

Pages a week: 4.3M · Median time: 610 ms · 4-week growth: +17%

[More about Text](https://grabber.sh/html-to-text)

### Links

`formats=links`

Every link on the page as a JSON list, ready for crawlers and site maps.

Pages a week: 2.7M · Median time: 590 ms · 4-week growth: +11%

[More about Links](https://grabber.sh/link-extractor)

### Metadata

`includeMetadata=true`

Title, description, language, Open Graph tags, JSON-LD, emails and phone numbers.

Pages a week: 1.7M · Median time: 630 ms · 4-week growth: +17%

[More about Metadata](https://grabber.sh/metadata-extractor)

### JavaScript rendering

`js_render=true`

Runs the page in a headless browser first, for single-page apps and content that loads late.

Pages a week: 2.4M · Median time: 2.4 s · 4-week growth: +10%

[More about JavaScript rendering](https://grabber.sh/javascript-rendering)

## Trusted in production

Agents, RAG pipelines and crawlers call Grabber around the clock. This is the last year.

### Pages fetched per week, in millions

| Week ending | Markdown | Text | Links | Metadata | JavaScript rendering | Total |
| --- | --- | --- | --- | --- | --- | --- |
| Nov 23, 2025 | 3.6M | 1.2M | 0.8M | 0.3M | 0.4M | 6.4M |
| Dec 21, 2025 | 4.3M | 1.3M | 0.8M | 0.4M | 0.5M | 7.4M |
| Jan 18, 2026 | 5.0M | 1.5M | 0.9M | 0.4M | 0.6M | 8.4M |
| Feb 15, 2026 | 5.7M | 1.6M | 1.1M | 0.5M | 0.7M | 9.5M |
| Mar 15, 2026 | 6.5M | 1.8M | 1.1M | 0.6M | 0.8M | 10.8M |
| Apr 12, 2026 | 7.1M | 2.0M | 1.3M | 0.6M | 0.9M | 11.9M |
| May 10, 2026 | 8.1M | 2.1M | 1.4M | 0.8M | 1.1M | 13.5M |
| Jun 7, 2026 | 9.4M | 2.5M | 1.6M | 0.8M | 1.2M | 15.4M |
| Jul 5, 2026 | 10.9M | 2.7M | 1.7M | 1.0M | 1.4M | 17.7M |
| Aug 2, 2026 | 11.9M | 3.0M | 2.0M | 1.2M | 1.6M | 19.6M |
| Aug 30, 2026 | 13.9M | 3.3M | 2.3M | 1.3M | 1.9M | 22.6M |
| Sep 27, 2026 | 16.4M | 3.6M | 2.5M | 1.5M | 2.1M | 26.1M |
| Oct 25, 2026 | 18.0M | 4.3M | 2.7M | 1.7M | 2.4M | 29.1M |

### Response time without rendering

| Response time | Share of requests |
| --- | --- |
| < 250 ms | 9% |
| 250–500 ms | 27% |
| 500–750 ms | 31% |
| 750 ms–1 s | 15% |
| 1–1.5 s | 9% |
| 1.5–2 s | 4% |
| 2–3 s | 3% |
| 3 s + | 2% |

- **99.9%** API uptime over the last 90 days
- **98.7%** Requests that return a page
- **2.4 s** Median time with JavaScript rendering
- **$0** Charged for a failed request

## One request. Any language.

Send a URL with your key and get Markdown back. Anything that can make an HTTP request can use Grabber, and MCP clients can call it as a tool.

- One endpoint: GET /fetch with a Bearer key
- JSON with every format, or unwrap=true for the raw content
- X-Grabber-Cost and X-Grabber-Balance on every answer

### cURL

```bash
curl -G https://api.grabber.sh/fetch \
  -H "Authorization: Bearer $GRABBER_API_KEY" \
  --data-urlencode "url=https://example.com/" \
  -d "formats=markdown" \
  -d "unwrap=true"
```

### Python

```python
import os
import requests

response = requests.get(
    "https://api.grabber.sh/fetch",
    params={"url": "https://example.com/", "formats": "markdown", "unwrap": "true"},
    headers={"Authorization": f"Bearer {os.environ['GRABBER_API_KEY']}"},
    timeout=130,
)
response.raise_for_status()
print(response.text)
```

### Node.js

```javascript
const params = new URLSearchParams({ url: 'https://example.com/', formats: 'markdown', unwrap: 'true' })

const response = await fetch(`https://api.grabber.sh/fetch?${params}`, {
  headers: { Authorization: `Bearer ${process.env.GRABBER_API_KEY}` },
  signal: AbortSignal.timeout(130_000),
})
if (!response.ok) throw new Error(`Grabber answered ${response.status}`)
console.log(await response.text())
```

### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	query := url.Values{"url": {"https://example.com/"}, "formats": {"markdown"}, "unwrap": {"true"}}
	req, _ := http.NewRequest("GET", "https://api.grabber.sh/fetch?"+query.Encode(), nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("GRABBER_API_KEY"))

	client := &http.Client{Timeout: 130 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status)
	fmt.Println(string(body))
}
```

### PHP

```php
<?php
$query = http_build_query(['url' => 'https://example.com/', 'formats' => 'markdown', 'unwrap' => 'true']);

$ch = curl_init("https://api.grabber.sh/fetch?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GRABBER_API_KEY')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 130,
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body;
```

### MCP

```json
{
  "mcpServers": {
    "grabber": {
      "url": "https://mcp.grabber.sh",
      "headers": {
        "Authorization": "Bearer gsh_your_api_key"
      }
    }
  }
}
```

### Response headers

```http
HTTP/2 200
content-type: text/markdown
cache-control: no-store
x-grabber-cost: 0.001
x-grabber-balance: 4.213
```

## Start in three steps

1. **Sign up.** Create an account with your email. You get $1 of free credit, enough for 1,000 pages, with no credit card.
2. **Add credit when you need it.** Top up from $5 by card, or turn on auto top-up. Credit works for every format and lasts 365 days.
3. **Get your API key.** Create a gsh_ key in the dashboard and send it as a Bearer token. It is plain HTTP, so any language or agent framework works.

## Built for agents

Grabber describes itself in the formats agents read, so an agent can find it, learn it and call it on its own.

- [MCP server](https://grabber.sh/mcp): A remote server with a fetch_page tool and a free check_balance tool.
- [llms.txt](https://grabber.sh/llms.txt): A Markdown map of every page on this site.
- [skill.md](https://grabber.sh/skill.md): Setup steps an agent can follow without a human.
- [OpenAPI](https://grabber.sh/openapi.json): The full API contract, for code generators and tool builders.

## Frequently asked questions

### Do I need an official developer account?

No. Grabber loads public web pages for you, so you do not need a developer account, a partner API or approval from the site you are reading. One Grabber API key works for every site.

### Is there a monthly minimum or subscription?

No. Grabber is prepaid. You add credit when you want it, from $5, and each request takes its price from that credit. There is no plan, no monthly minimum and nothing to cancel.

### Can I pay for a single request?

Yes. Every request is billed on its own: $0.001 for a page, or $0.005 when JavaScript rendering is on. New accounts get $1 of free credit, enough for 1,000 pages, with no credit card.

### Am I charged for failed requests?

No. You pay only for a page that was loaded. Any error, such as a timeout, a blocked address or a page that could not be reached, costs nothing. A page whose own server answered 404 or 500 still counts as loaded, because you get its content and its status code.

### What formats can I get back?

Markdown, HTML, plain text, the list of links on the page, or the raw body. You can ask for several in one request, and add page metadata such as the title, description, Open Graph tags and JSON-LD.

### Does Grabber run JavaScript?

Yes, when you ask for it. Add js_render=true and the page runs in a headless browser before it is converted, so content that only appears after scripts run is included. A rendered page costs $0.005 and can take up to 120 seconds.

### Can Claude, Cursor or my own agent use Grabber directly?

Yes. Grabber runs a remote MCP server at https://mcp.grabber.sh. Add it to any MCP client with your API key, and your agent gets a fetch_page tool that returns clean Markdown, plus a free check_balance tool.

### What are the rate limits?

60 requests per minute per API key. Over the limit, the API answers 429 with a Retry-After header that says how many seconds to wait.

### Do credits expire?

Credit is valid for 365 days after you add it. Every response carries X-Grabber-Cost and X-Grabber-Balance headers, and the dashboard lists each request with its cost.

### What can Grabber not fetch?

Grabber reads public pages and does not sign in to sites, so a page behind a login returns what a signed-out visitor sees. Private and local network addresses, cloud metadata hosts and bodies larger than 8 MiB are refused, and a refused request costs nothing.

## From the blog

Guides for getting web pages into models, with the code and the cost.

- [Give Claude and Cursor Web Access With an MCP Server](https://grabber.sh/blog/mcp-web-access-for-claude-and-cursor): Connect Cursor, Claude Code, Claude Desktop, VS Code or Windsurf to the web with Grabber's remote MCP server. Setup, tools, prompts and cost. (October 27, 2026)
- [Static Fetch vs JavaScript Rendering: When You Need a Headless Browser](https://grabber.sh/blog/static-fetch-vs-javascript-rendering): Most pages don't need a headless browser. How to tell when one does, what each costs, and a static-first fallback that cuts the bill. (October 27, 2026)
- [Turn Any Web Page Into Clean Markdown for RAG](https://grabber.sh/blog/web-page-to-markdown-for-rag): Why Markdown beats raw HTML for retrieval, how to fetch any URL as Markdown, and how to chunk it by headings in Python and Node.js. (October 27, 2026)

[All posts](https://grabber.sh/blog)

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/

---

# Pay for pages, not plans

> Grabber costs $0.001 per page and $0.005 with JavaScript rendering. Prepaid credit, no subscription, no minimum, failed requests free.

$0.001 per page, or $0.005 when the page needs JavaScript. No subscription, no monthly minimum, and you never pay for a request that failed.

- $1 free credit, no card
- No subscription
- Failed requests are free

[Get API key](https://dashboard.grabber.sh/signup) · [Pricing in the docs](https://grabber.sh/docs/pricing-and-limits)

## Two prices. That is the whole list.

Every request takes its price from your prepaid credit. Formats, metadata and redirects are included.

### Page

**$0.001** per page

Any public page or PDF, as Markdown, HTML, text, links or the raw body.

- All five formats in one request
- Page metadata when you ask for it
- 1,000 pages for $1

### Rendered page

**$0.005** per page

For pages built by JavaScript: the page runs in a headless browser first.

- Add js_render=true to any request
- Up to 120 seconds per page
- 200 pages for $1

### Free credit

**$1** when you sign up

Verify your email and start with free credit. No card needed.

- About 1,000 pages
- The same API, formats and limits
- Add credit only when you need more

## Estimate your monthly cost

Pick how many pages you fetch in a month and how many of them need JavaScript.

| Pages a month | Rendered with JavaScript | Cost a month |
| --- | --- | --- |
| 10,000 | 0% | $10.00 |
| 100,000 | 0% | $100.00 |
| 100,000 | 10% | $140.00 |
| 1,000,000 | 10% | $1,400.00 |
| 1,000,000 | 50% | $3,000.00 |

Estimates count successful requests only: errors cost nothing.

## Never on your bill

### Failed requests

Timeouts, blocked addresses, pages that would not load and Grabber's own failures all cost $0. You pay for a page only when you get it.

### Subscriptions

No plan, no seats and no monthly minimum. There is nothing to cancel and nothing to downgrade.

### Card fees

Grabber pays Stripe's fee, so $25 paid is $25 of credit.

### Extra formats

Markdown, HTML, text and links in one request cost the same as one of them: it is one page load.

## Credit and top-ups

| Item | How it works |
| --- | --- |
| Smallest top-up | $5 |
| Quick amounts | $10, $25, $50, $100 |
| Auto top-up | When the balance falls below $5, add $25. You choose both amounts. |
| Payment | Card, through Stripe. Invoices are in the dashboard. |
| Credit lifetime | 365 days from when you add it |
| Spend tracking | Every response has `X-Grabber-Cost` and `X-Grabber-Balance`, and the dashboard lists each request. |

## Pricing questions

### Is there a monthly minimum or subscription?

No. Grabber is prepaid. You add credit when you want it, from $5, and each request takes its price from that credit. There is no plan, no monthly minimum and nothing to cancel.

### Can I pay for a single request?

Yes. Every request is billed on its own: $0.001 for a page, or $0.005 when JavaScript rendering is on. New accounts get $1 of free credit, enough for 1,000 pages, with no credit card.

### Am I charged for failed requests?

No. You pay only for a page that was loaded. Any error, such as a timeout, a blocked address or a page that could not be reached, costs nothing. A page whose own server answered 404 or 500 still counts as loaded, because you get its content and its status code.

### What happens when my credit runs out?

Requests answer 402 with a link to add credit, and nothing is charged. Turn on auto top-up and your balance is refilled before it runs out.

### Do credits expire?

Credit is valid for 365 days after you add it. Every response carries X-Grabber-Cost and X-Grabber-Balance headers, and the dashboard lists each request with its cost.

### Do I get an invoice?

Yes. Every payment has an invoice on the Billing page of the dashboard.

## Start with 1,000 free pages

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/pricing

---

# Turn any URL into clean Markdown

> Convert any web page or PDF into clean Markdown with one API call, ready for LLMs, RAG and AI agents. $0.001 per page, no subscription, errors free.

Send a URL and get the content back as Markdown. Headings, lists, tables, links and code come through; scripts, styles and tag soup do not. Ready for a prompt, a vector store or an agent.

- $1 free credit, no card
- No subscription
- Failed requests are free

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs/parameters)

## One request, clean Markdown

Add unwrap=true and the Markdown is the whole response body, with Content-Type: text/markdown.

- Works on HTML pages and PDFs
- Follows redirects to the final page
- Add links or metadata in the same request

### cURL

```bash
curl -G https://api.grabber.sh/fetch \
  -H "Authorization: Bearer $GRABBER_API_KEY" \
  --data-urlencode "url=https://example.com/blog/rag-guide" \
  -d "formats=markdown" \
  -d "unwrap=true"
```

### Python

```python
import os
import requests

response = requests.get(
    "https://api.grabber.sh/fetch",
    params={"url": "https://example.com/blog/rag-guide", "formats": "markdown", "unwrap": "true"},
    headers={"Authorization": f"Bearer {os.environ['GRABBER_API_KEY']}"},
    timeout=130,
)
response.raise_for_status()
print(response.text)
```

### Node.js

```javascript
const params = new URLSearchParams({ url: 'https://example.com/blog/rag-guide', formats: 'markdown', unwrap: 'true' })

const response = await fetch(`https://api.grabber.sh/fetch?${params}`, {
  headers: { Authorization: `Bearer ${process.env.GRABBER_API_KEY}` },
  signal: AbortSignal.timeout(130_000),
})
if (!response.ok) throw new Error(`Grabber answered ${response.status}`)
console.log(await response.text())
```

### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	query := url.Values{"url": {"https://example.com/blog/rag-guide"}, "formats": {"markdown"}, "unwrap": {"true"}}
	req, _ := http.NewRequest("GET", "https://api.grabber.sh/fetch?"+query.Encode(), nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("GRABBER_API_KEY"))

	client := &http.Client{Timeout: 130 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status)
	fmt.Println(string(body))
}
```

### PHP

```php
<?php
$query = http_build_query(['url' => 'https://example.com/blog/rag-guide', 'formats' => 'markdown', 'unwrap' => 'true']);

$ch = curl_init("https://api.grabber.sh/fetch?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GRABBER_API_KEY')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 130,
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body;
```

### Response body

```markdown
# A practical guide to RAG

Retrieval-augmented generation answers questions from your own documents.

## 1. Split pages into chunks

- Keep each heading with its text
- Aim for 300 to 800 tokens per chunk

| Chunk size | Recall |
| --- | --- |
| 300 tokens | 0.81 |

[Next: choosing embeddings](https://example.com/blog/embeddings)
```

## How it works

1. **Send the URL.** Call `GET /fetch` with `url` and `formats=markdown`. Add `unwrap=true` to get the Markdown as the body.
2. **Grabber loads the page.** It follows redirects, reads the HTML or the PDF, and converts the content to Markdown.
3. **Use the Markdown.** Put it in a prompt, split it into chunks for a vector store, or save it. `X-Grabber-Cost` says what it cost.

## What people build with it

### RAG pipelines

Index documentation, help centers and articles as Markdown, so chunks keep their headings and tables.

### Agents that browse

Give an agent a tool that reads a page in a few thousand tokens instead of tens of thousands of HTML.

### Docs for coding assistants

Feed the current docs of a library to Cursor or Claude, so they code against the version you use.

### Content migration

Move pages from an old site into a Markdown-based CMS or a static site generator.

## Pricing

Prepaid credit, taken one request at a time. Ask for several formats in one request at the price of one.

| Request | Price |
| --- | --- |
| A page | **$0.001** |
| A page with `js_render=true` | **$0.005** |
| Any error | $0 |
| Credit when you sign up | $1, about 1,000 pages |

## Questions

### Why Markdown instead of HTML for an LLM?

Markdown keeps the structure a model needs, such as headings, lists, tables and links, and drops the markup it does not. The same page is usually several times smaller, so it costs fewer tokens and leaves more room in the context window.

### Does it work with PDFs?

Yes. Point url at a PDF and you get its text back as Markdown, at the same price as a web page.

### Can I get Markdown and links in one request?

Yes. Ask for formats=markdown,links and both come back in the JSON envelope. It is one page load, so it costs $0.001, the same as one format.

### What if the page is built with JavaScript?

Add js_render=true. The page runs in a headless browser first, and the Markdown is made from what it shows. A rendered page costs $0.005.

## Related

- [HTML to text](https://grabber.sh/html-to-text): Plain text with no markup at all.
- [JavaScript rendering](https://grabber.sh/javascript-rendering): Pages built in the browser, rendered first.
- [Link extractor](https://grabber.sh/link-extractor): Every link on a page, as absolute URLs.
- [MCP server](https://grabber.sh/mcp): The same API as a tool for your agent.

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/url-to-markdown

---

# Plain text from any web page

> Extract clean plain text from any web page with one API call: no tags, no scripts, no styles. Built for search, classification and AI. $0.001 per page.

Send a URL and get back the readable content of the page as plain text, with every tag, script and style removed. For search indexes, classifiers and anything that only needs the words.

- $1 free credit, no card
- No subscription
- Failed requests are free

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs/parameters)

## One request, plain text

With unwrap=true the text is the whole response body, with Content-Type: text/plain.

- No markup to strip yourself
- Ask for text and links together
- Same price as every format

### cURL

```bash
curl -G https://api.grabber.sh/fetch \
  -H "Authorization: Bearer $GRABBER_API_KEY" \
  --data-urlencode "url=https://example.com/news/launch" \
  -d "formats=text" \
  -d "unwrap=true"
```

### Python

```python
import os
import requests

response = requests.get(
    "https://api.grabber.sh/fetch",
    params={"url": "https://example.com/news/launch", "formats": "text", "unwrap": "true"},
    headers={"Authorization": f"Bearer {os.environ['GRABBER_API_KEY']}"},
    timeout=130,
)
response.raise_for_status()
print(response.text)
```

### Node.js

```javascript
const params = new URLSearchParams({ url: 'https://example.com/news/launch', formats: 'text', unwrap: 'true' })

const response = await fetch(`https://api.grabber.sh/fetch?${params}`, {
  headers: { Authorization: `Bearer ${process.env.GRABBER_API_KEY}` },
  signal: AbortSignal.timeout(130_000),
})
if (!response.ok) throw new Error(`Grabber answered ${response.status}`)
console.log(await response.text())
```

### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	query := url.Values{"url": {"https://example.com/news/launch"}, "formats": {"text"}, "unwrap": {"true"}}
	req, _ := http.NewRequest("GET", "https://api.grabber.sh/fetch?"+query.Encode(), nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("GRABBER_API_KEY"))

	client := &http.Client{Timeout: 130 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status)
	fmt.Println(string(body))
}
```

### PHP

```php
<?php
$query = http_build_query(['url' => 'https://example.com/news/launch', 'formats' => 'text', 'unwrap' => 'true']);

$ch = curl_init("https://api.grabber.sh/fetch?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GRABBER_API_KEY')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 130,
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body;
```

### Response body

```text
Example Co. launches its new trail shoe

The Trail Runner 2 weighs 240 grams and ships on November 3.

Pre-orders open today on the Example store, with free returns for 60 days.
```

## How it works

1. **Send the URL.** Call `GET /fetch` with `url` and `formats=text`.
2. **Grabber loads the page.** It follows redirects, loads the page and removes the markup, scripts and styles.
3. **Use the text.** Index it, classify it, embed it or compare it with yesterday. Errors cost nothing.

## What people build with it

### Search indexes

Fill a full-text search engine with the words of each page and nothing else.

### Classification

Run topic, language or sentiment models on clean input instead of HTML.

### Embeddings

Embed pages for similarity search when you do not need their headings or tables.

### Change detection

Compare the text of a page over time, without layout changes showing up as edits.

## Pricing

Prepaid credit, taken one request at a time. Ask for several formats in one request at the price of one.

| Request | Price |
| --- | --- |
| A page | **$0.001** |
| A page with `js_render=true` | **$0.005** |
| Any error | $0 |
| Credit when you sign up | $1, about 1,000 pages |

## Questions

### What is the difference between text and markdown?

Both hold the readable content of the page. Markdown keeps its structure, such as headings, lists, tables and links. Text has no markup at all. Use Markdown for language models and text when you only need the words.

### Can I get the text and the links together?

Yes. Ask for formats=text,links and both come back in the JSON envelope, for $0.001.

### Does it work on pages built with JavaScript?

Yes, with js_render=true. The page runs in a headless browser first, for $0.005 a page.

### What happens when a page will not load?

You get an error with a stable code, such as fetch_failed, and nothing is charged.

## Related

- [URL to Markdown](https://grabber.sh/url-to-markdown): Clean Markdown for LLMs and RAG.
- [Link extractor](https://grabber.sh/link-extractor): Every link on a page, as absolute URLs.
- [Metadata extractor](https://grabber.sh/metadata-extractor): Title, Open Graph tags, JSON-LD and more.
- [All parameters](https://grabber.sh/docs/parameters): Every option of GET /fetch.

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/html-to-text

---

# Extract every link from any page

> Get every link on a web page as a JSON list of absolute URLs, with one API call. Build crawlers, audits and agents that navigate. $0.001 per page.

Send a URL and get back a JSON array of every link on the page, as absolute URLs. The building block for crawlers, sitemap checks and agents that need to know where to go next.

- $1 free credit, no card
- No subscription
- Failed requests are free

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs/parameters)

## One request, every link

With unwrap=true the response body is the JSON array itself.

- Absolute URLs, ready to fetch
- From the final page, after redirects
- Combine with markdown in one request

### cURL

```bash
curl -G https://api.grabber.sh/fetch \
  -H "Authorization: Bearer $GRABBER_API_KEY" \
  --data-urlencode "url=https://example.com/" \
  -d "formats=links" \
  -d "unwrap=true"
```

### Python

```python
import os
import requests

response = requests.get(
    "https://api.grabber.sh/fetch",
    params={"url": "https://example.com/", "formats": "links", "unwrap": "true"},
    headers={"Authorization": f"Bearer {os.environ['GRABBER_API_KEY']}"},
    timeout=130,
)
response.raise_for_status()
print(response.text)
```

### Node.js

```javascript
const params = new URLSearchParams({ url: 'https://example.com/', formats: 'links', unwrap: 'true' })

const response = await fetch(`https://api.grabber.sh/fetch?${params}`, {
  headers: { Authorization: `Bearer ${process.env.GRABBER_API_KEY}` },
  signal: AbortSignal.timeout(130_000),
})
if (!response.ok) throw new Error(`Grabber answered ${response.status}`)
console.log(await response.text())
```

### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	query := url.Values{"url": {"https://example.com/"}, "formats": {"links"}, "unwrap": {"true"}}
	req, _ := http.NewRequest("GET", "https://api.grabber.sh/fetch?"+query.Encode(), nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("GRABBER_API_KEY"))

	client := &http.Client{Timeout: 130 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status)
	fmt.Println(string(body))
}
```

### PHP

```php
<?php
$query = http_build_query(['url' => 'https://example.com/', 'formats' => 'links', 'unwrap' => 'true']);

$ch = curl_init("https://api.grabber.sh/fetch?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GRABBER_API_KEY')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 130,
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body;
```

### Response body

```json
[
  "https://example.com/pricing",
  "https://example.com/docs",
  "https://example.com/blog",
  "https://example.com/blog/rag-guide",
  "https://github.com/example"
]
```

## How it works

1. **Send the URL.** Call `GET /fetch` with `url` and `formats=links`.
2. **Grabber reads the page.** It follows redirects, loads the page and collects every link on it as an absolute URL.
3. **Decide what is next.** Filter the list, then fetch the pages you want. Each fetch is its own request.

## What people build with it

### Crawlers

Discover pages one level at a time and decide yourself which ones to follow.

### SEO audits

Check internal links, find pages that nothing links to, and spot links to old domains.

### Agents that navigate

Let an agent see the links on a page so it can choose the next one to read.

### Monitoring

Watch a listing or changelog page and act when a new link appears.

## Pricing

Prepaid credit, taken one request at a time. Ask for several formats in one request at the price of one.

| Request | Price |
| --- | --- |
| A page | **$0.001** |
| A page with `js_render=true` | **$0.005** |
| Any error | $0 |
| Credit when you sign up | $1, about 1,000 pages |

## Questions

### Are relative links turned into full URLs?

Yes. Every link comes back as an absolute URL, so you can fetch it as it is.

### Does Grabber crawl the links for me?

No. Grabber loads one page per request, so you stay in control of what is fetched and what it costs. Fetch the next pages yourself, up to 60 requests per minute per key.

### Can I get the links and the content together?

Yes. Ask for formats=markdown,links and you get both in the JSON envelope, for $0.001.

### What about links that JavaScript adds?

Add js_render=true and the links are read after the page has run in a headless browser, for $0.005 a page.

## Related

- [URL to Markdown](https://grabber.sh/url-to-markdown): Clean Markdown for LLMs and RAG.
- [Metadata extractor](https://grabber.sh/metadata-extractor): Title, Open Graph tags, JSON-LD and more.
- [JavaScript rendering](https://grabber.sh/javascript-rendering): Pages built in the browser, rendered first.
- [All parameters](https://grabber.sh/docs/parameters): Every option of GET /fetch.

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/link-extractor

---

# Page metadata in one request

> Extract the title, description, Open Graph tags, JSON-LD, emails and phone numbers of any URL with one API call. $0.001 per page, errors free.

Add includeMetadata=true to any request and get the metadata of the page as JSON: title, description, language, canonical URL, Open Graph tags, JSON-LD, links, emails and phone numbers.

- $1 free credit, no card
- No subscription
- Failed requests are free

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs/parameters)

## One request, structured metadata

Metadata comes in the JSON envelope, next to the format you ask for.

- JSON-LD parsed into objects
- Every Open Graph tag
- No extra charge

### cURL

```bash
curl -G https://api.grabber.sh/fetch \
  -H "Authorization: Bearer $GRABBER_API_KEY" \
  --data-urlencode "url=https://shop.example.com/products/trail-runner-2" \
  -d "formats=links" \
  -d "includeMetadata=true"
```

### Python

```python
import os
import requests

response = requests.get(
    "https://api.grabber.sh/fetch",
    params={"url": "https://shop.example.com/products/trail-runner-2", "formats": "links", "includeMetadata": "true"},
    headers={"Authorization": f"Bearer {os.environ['GRABBER_API_KEY']}"},
    timeout=130,
)
response.raise_for_status()
print(response.text)
```

### Node.js

```javascript
const params = new URLSearchParams({ url: 'https://shop.example.com/products/trail-runner-2', formats: 'links', includeMetadata: 'true' })

const response = await fetch(`https://api.grabber.sh/fetch?${params}`, {
  headers: { Authorization: `Bearer ${process.env.GRABBER_API_KEY}` },
  signal: AbortSignal.timeout(130_000),
})
if (!response.ok) throw new Error(`Grabber answered ${response.status}`)
console.log(await response.text())
```

### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	query := url.Values{"url": {"https://shop.example.com/products/trail-runner-2"}, "formats": {"links"}, "includeMetadata": {"true"}}
	req, _ := http.NewRequest("GET", "https://api.grabber.sh/fetch?"+query.Encode(), nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("GRABBER_API_KEY"))

	client := &http.Client{Timeout: 130 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status)
	fmt.Println(string(body))
}
```

### PHP

```php
<?php
$query = http_build_query(['url' => 'https://shop.example.com/products/trail-runner-2', 'formats' => 'links', 'includeMetadata' => 'true']);

$ch = curl_init("https://api.grabber.sh/fetch?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GRABBER_API_KEY')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 130,
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body;
```

### Response (data.metadata)

```json
{
  "canonicalUrl": "https://shop.example.com/products/trail-runner-2",
  "title": "Trail Runner 2 | Example Shop",
  "description": "A 240 g trail shoe with a grippy outsole.",
  "languageCode": "en",
  "openGraph": [
    {
      "property": "og:image",
      "content": "https://shop.example.com/img/tr2.jpg"
    }
  ],
  "jsonLd": [
    {
      "@type": "Product",
      "name": "Trail Runner 2",
      "offers": {
        "price": "129.00",
        "priceCurrency": "USD"
      }
    }
  ],
  "emails": [
    "support@example.com"
  ]
}
```

## How it works

1. **Send the URL.** Call `GET /fetch` with `url`, `includeMetadata=true` and the format you also want.
2. **Grabber reads the page.** It loads the page and reads its meta tags, Open Graph tags and JSON-LD blocks, and finds emails and phone numbers.
3. **Use the fields.** Every field is always present: a value the page does not have is null or an empty list.

## What people build with it

### Link previews

Show a title, description and image for any URL your users paste.

### Product data

Read prices, names and availability from the Product JSON-LD that shops publish.

### Directories and CRMs

Fill in the name, description and contact details of a company from its own site.

### SEO checks

Find pages with a missing description, a wrong canonical URL or broken structured data.

## Pricing

Prepaid credit, taken one request at a time. Ask for several formats in one request at the price of one.

| Request | Price |
| --- | --- |
| A page | **$0.001** |
| A page with `js_render=true` | **$0.005** |
| Any error | $0 |
| Credit when you sign up | $1, about 1,000 pages |

## Questions

### Which fields do I get?

canonicalUrl, title, description, author, keywords, languageCode, openGraph, jsonLd, links, phones and emails. Every field is always there; one the page does not have is null or an empty list.

### Is the JSON-LD parsed?

Yes. Each JSON-LD block on the page comes back as a JSON object, ready to read.

### Does metadata cost extra?

No. It is part of the same page load, so a request with metadata costs $0.001, like any other.

### Can I use it with unwrap=true?

No. Metadata lives in the JSON envelope, so includeMetadata is ignored when unwrap is true. Leave unwrap off and read data.metadata.

## Related

- [Link extractor](https://grabber.sh/link-extractor): Every link on a page, as absolute URLs.
- [URL to Markdown](https://grabber.sh/url-to-markdown): Clean Markdown for LLMs and RAG.
- [HTML to text](https://grabber.sh/html-to-text): Plain text with no markup at all.
- [All parameters](https://grabber.sh/docs/parameters): Every option of GET /fetch.

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/metadata-extractor

---

# Render JavaScript before you read the page

> Load React, Vue and other single-page apps in a headless browser and get the rendered page as Markdown, HTML or text. $0.005 per page, errors free.

Some pages are empty until their scripts run. Add js_render=true and Grabber runs the page in a headless browser first, then returns what it shows in any format.

- $1 free credit, no card
- No subscription
- Failed requests are free

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs/parameters)

## One parameter, the rendered page

Add js_render=true to any request. Everything else stays the same.

- Works with every format
- Up to 120 seconds per page
- $0.005 per page, only when you ask

### cURL

```bash
curl -G https://api.grabber.sh/fetch \
  -H "Authorization: Bearer $GRABBER_API_KEY" \
  --data-urlencode "url=https://app.example.com/pricing" \
  -d "formats=markdown" \
  -d "js_render=true" \
  -d "unwrap=true"
```

### Python

```python
import os
import requests

response = requests.get(
    "https://api.grabber.sh/fetch",
    params={"url": "https://app.example.com/pricing", "formats": "markdown", "js_render": "true", "unwrap": "true"},
    headers={"Authorization": f"Bearer {os.environ['GRABBER_API_KEY']}"},
    timeout=130,
)
response.raise_for_status()
print(response.text)
```

### Node.js

```javascript
const params = new URLSearchParams({ url: 'https://app.example.com/pricing', formats: 'markdown', js_render: 'true', unwrap: 'true' })

const response = await fetch(`https://api.grabber.sh/fetch?${params}`, {
  headers: { Authorization: `Bearer ${process.env.GRABBER_API_KEY}` },
  signal: AbortSignal.timeout(130_000),
})
if (!response.ok) throw new Error(`Grabber answered ${response.status}`)
console.log(await response.text())
```

### Go

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	query := url.Values{"url": {"https://app.example.com/pricing"}, "formats": {"markdown"}, "js_render": {"true"}, "unwrap": {"true"}}
	req, _ := http.NewRequest("GET", "https://api.grabber.sh/fetch?"+query.Encode(), nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("GRABBER_API_KEY"))

	client := &http.Client{Timeout: 130 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status)
	fmt.Println(string(body))
}
```

### PHP

```php
<?php
$query = http_build_query(['url' => 'https://app.example.com/pricing', 'formats' => 'markdown', 'js_render' => 'true', 'unwrap' => 'true']);

$ch = curl_init("https://api.grabber.sh/fetch?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GRABBER_API_KEY')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 130,
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body;
```

### Response body

```markdown
# Pricing

| Plan | Price | Seats |
| --- | --- | --- |
| Starter | $19 a month | 3 |
| Team | $49 a month | 10 |

All plans include a 14-day trial.
```

## How it works

1. **Add js_render=true.** Keep the rest of your request as it is: the URL, the formats and unwrap.
2. **Grabber runs the page.** The page loads in a headless browser and its scripts run, so the content they build is there.
3. **You get what it shows.** The rendered page comes back in the formats you asked for. Allow up to 120 seconds.

## What people build with it

### Single-page apps

Read sites built with React, Vue, Angular or Svelte that send an empty page and fill it in the browser.

### Pricing and plan pages

Many pricing tables are loaded by scripts. Rendering gets you the numbers, not a spinner.

### Content loaded later

Reviews, listings and comments that arrive after the first load are in the rendered page.

### Agents on modern sites

Let your agent retry with js_render when a page comes back empty.

## Pricing

Prepaid credit, taken one request at a time. Ask for several formats in one request at the price of one.

| Request | Price |
| --- | --- |
| A page | **$0.001** |
| A page with `js_render=true` | **$0.005** |
| Any error | $0 |
| Credit when you sign up | $1, about 1,000 pages |

## Questions

### When do I need JavaScript rendering?

When a page comes back empty or says to enable JavaScript. Try without it first: a plain fetch is faster and costs $0.001 instead of $0.005.

### How long does a rendered page take?

Usually a few seconds, and never more than 120. Set your client timeout to at least 130 seconds.

### Can it sign in or solve CAPTCHAs?

No. Grabber reads public pages as a signed-out visitor, with or without rendering.

### Am I charged if the render fails?

No. Any error, including a render that times out, costs nothing.

## Related

- [URL to Markdown](https://grabber.sh/url-to-markdown): Clean Markdown for LLMs and RAG.
- [Link extractor](https://grabber.sh/link-extractor): Every link on a page, as absolute URLs.
- [HTML to text](https://grabber.sh/html-to-text): Plain text with no markup at all.
- [MCP server](https://grabber.sh/mcp): The same API as a tool for your agent.

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/javascript-rendering

---

# How Grabber compares

> Grabber compared with Firecrawl, Jina Reader, Tavily Extract, ScrapingBee, Scrape.do, Apify and ZenRows on price, plans, free tiers and MCP.

Seven services that turn web pages into data, side by side with Grabber. Each comparison says where the other service is the better choice, too.

[Get API key](https://dashboard.grabber.sh/signup) · [Pricing](https://grabber.sh/pricing)

## The short version

| Service | How you pay | A static page | Subscription | Unused credit | MCP server |
| --- | --- | --- | --- | --- | --- |
| **Grabber** | Prepaid credit | $0.001 | Not needed | Lasts 365 days | Remote |
| [Firecrawl](https://grabber.sh/alternatives/firecrawl) | Monthly plans | About $0.0038 on Hobby | Needed for extra credit | Does not roll over | Remote and local |
| [Jina Reader](https://grabber.sh/alternatives/jina-reader) | Prepaid tokens | By length, often under $0.001 | Not needed | Not stated | Remote |
| [Tavily Extract](https://grabber.sh/alternatives/tavily-extract) | Credits | About $0.0016 | Not needed | Resets monthly | Remote and local |
| [ScrapingBee](https://grabber.sh/alternatives/scrapingbee) | Monthly plans | About $0.00025 on Hobby | Needed | Valid one month | Remote |
| [Scrape.do](https://grabber.sh/alternatives/scrape-do) | Monthly plans | About $0.00012 on Hobby | Needed | Not stated | Third-party only |
| [Apify Website Content Crawler](https://grabber.sh/alternatives/apify) | Compute time | About $0.0002, estimated | Needed for overage | Does not roll over | Remote and local |
| [ZenRows](https://grabber.sh/alternatives/zenrows) | Monthly plans | About $0.00042 on Build | Needed | Expires at renewal | Remote and local |

Prices from each service's own pricing page, checked on October 27, 2026. Per-page prices on a plan assume you use the whole plan.

## How we compare

Every fact comes from the other service's own pricing page and docs, and each comparison lists its sources. Where a page did not say something, we wrote "not stated" instead of guessing.

The short answer: Grabber costs less when your volume is small or uneven, because there is no plan and credit lasts a year. If you fetch the same large volume every month, or you need proxies, crawling or search, one of these services may suit you better, and the comparison will say so.

## Comparisons

- [Grabber vs Firecrawl](https://grabber.sh/alternatives/firecrawl): Firecrawl is a web data platform with crawling, search and LLM extraction, sold as monthly plans.
- [Grabber vs Jina Reader](https://grabber.sh/alternatives/jina-reader): Jina Reader turns a URL into Markdown when you put r.jina.ai in front of it, and charges by the tokens it returns.
- [Grabber vs Tavily Extract](https://grabber.sh/alternatives/tavily-extract): Tavily is a search API for agents, and Extract is its endpoint for reading pages, up to 20 at a time.
- [Grabber vs ScrapingBee](https://grabber.sh/alternatives/scrapingbee): ScrapingBee is a scraping API with rotating proxies and rendering on by default, on monthly plans from $19.
- [Grabber vs Scrape.do](https://grabber.sh/alternatives/scrape-do): Scrape.do is a proxy-based scraping API built to get past anti-bot systems, on monthly plans from $29.
- [Grabber vs Apify](https://grabber.sh/alternatives/apify): Apify's Website Content Crawler crawls whole sites into Markdown for RAG, billed by compute time on Apify's platform.
- [Grabber vs ZenRows](https://grabber.sh/alternatives/zenrows): ZenRows Fetch, formerly the Universal Scraper API, reads pages behind anti-bot systems, on monthly plans from $19.

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/alternatives

---

# Grabber vs Firecrawl

> Grabber vs Firecrawl: price per page, plans, free tiers, failed requests, formats and MCP, side by side. Checked October 2026.

Firecrawl is a web data platform with crawling, search and LLM extraction, sold as monthly plans. Grabber does one thing, one page per request, and you pay per page with no plan.

[Get API key](https://dashboard.grabber.sh/signup) · [All comparisons](https://grabber.sh/alternatives)

## At a glance

|  | Grabber | Firecrawl |
| --- | --- | --- |
| How you pay | Prepaid credit, from $5. No plans | Monthly credit plans: Hobby $19 for 5,000 credits, Standard $99 for 100,000 |
| Pay without a subscription | Yes: it is the only way to pay | No. Extra credit needs a paid plan |
| A static page | $0.001 | 1 credit: about $0.0038 on Hobby, $0.00099 on Standard |
| A JavaScript page | $0.005 with js_render=true | 1 credit: rendering is included |
| Failed requests | Never charged. A page that loads with a 404 or 500 status is charged | Not charged when there is no result. A page with a 403 or 404 status costs 1 credit |
| Unused credit | Valid for 365 days | Monthly credits do not roll over on Hobby, Standard and Growth |
| Free tier | $1 at signup, about 1,000 pages, no card | 1,000 credits a month, no card |
| Formats | Markdown, HTML, text, links, raw body, page metadata | Markdown, HTML, raw HTML, links, screenshots, summaries, JSON extraction |
| MCP server | Yes, remote | Yes, remote and local |
| Rate limit | 60 requests a minute per key | 100 scrapes a minute on Hobby |

Checked on October 27, 2026 against Firecrawl's own pricing page and docs. Prices change, so check the sources below before you decide.

## What it costs

| Example | Grabber | Firecrawl |
| --- | --- | --- |
| 1,000 pages a month | $1 | $0 on the free plan |
| 10,000 pages a month | $10 | $44: Hobby plus 5,000 credits of top-ups |
| 100,000 pages a month | $100 | $99: Standard |

Monthly billing. Firecrawl renders JavaScript at no extra cost; Grabber charges $0.005 for a rendered page.

## When Firecrawl is the better choice

- You need to crawl or map whole sites, search the web, or extract JSON with an LLM. Grabber loads one page per request and does none of these.
- Most of your pages need JavaScript. Firecrawl includes rendering in its 1 credit, and Grabber charges $0.005 for a rendered page.
- You fetch 100,000 pages or more every month. A plan you use in full costs the same or less per page.
- You need screenshots, proxies in a given country, or actions on the page.

## When Grabber is the better choice

- Your volume changes from month to month. Grabber credit lasts 365 days, so a quiet month costs nothing and nothing expires at renewal.
- You want to pay without a plan. On Firecrawl, extra credit needs a paid plan; on Grabber, prepaid credit is the only plan.
- You fetch up to tens of thousands of pages a month: 10,000 pages cost $10 on Grabber and $44 on Firecrawl Hobby with top-ups.
- You want an API an agent can learn in one read: one endpoint and five parameters.

## Switching from Firecrawl

Firecrawl's /scrape takes a JSON body. Grabber takes query parameters on GET /fetch.

| Firecrawl | Grabber |
| --- | --- |
| `formats: ["markdown"]` | `formats=markdown` |
| `formats: ["html"]` | `formats=html` |
| `formats: ["links"]` | `formats=links` |
| Metadata in every response | `includeMetadata=true` |
| Rendering, built in | `js_render=true`, when a page needs it |

## Questions

### Is Grabber cheaper than Firecrawl?

Up to about 100,000 static pages a month, yes: 10,000 pages cost $10 on Grabber and $44 on Firecrawl Hobby with top-ups. Above that, a Firecrawl plan you use in full costs the same or less, and Firecrawl does not charge extra for JavaScript rendering.

### Does Grabber crawl websites like Firecrawl?

No. Grabber fetches one page per request. To crawl, ask for formats=links and fetch the links you want next.

### Do both have an MCP server?

Yes. Firecrawl has remote and local servers. Grabber has a remote server at mcp.grabber.sh with a fetch_page tool.

## Sources

- [Firecrawl pricing](https://www.firecrawl.dev/pricing)
- [Firecrawl billing docs](https://docs.firecrawl.dev/billing)
- [Firecrawl scrape docs](https://docs.firecrawl.dev/features/scrape)
- [Firecrawl rate limits](https://docs.firecrawl.dev/rate-limits)
- [Firecrawl MCP server](https://docs.firecrawl.dev/mcp-server)

Grabber's prices are on the [pricing page](https://grabber.sh/pricing) and in the [docs](https://grabber.sh/docs/pricing-and-limits).

## Other comparisons

- [Grabber vs Jina Reader](https://grabber.sh/alternatives/jina-reader): Prepaid tokens
- [Grabber vs Tavily Extract](https://grabber.sh/alternatives/tavily-extract): Credits
- [Grabber vs ScrapingBee](https://grabber.sh/alternatives/scrapingbee): Monthly plans
- [Grabber vs Scrape.do](https://grabber.sh/alternatives/scrape-do): Monthly plans
- [Grabber vs Apify](https://grabber.sh/alternatives/apify): Compute time
- [Grabber vs ZenRows](https://grabber.sh/alternatives/zenrows): Monthly plans

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/alternatives/firecrawl

---

# Grabber vs Jina Reader

> Grabber vs Jina Reader: price per page, plans, free tiers, failed requests, formats and MCP, side by side. Checked October 2026.

Jina Reader turns a URL into Markdown when you put r.jina.ai in front of it, and charges by the tokens it returns. Grabber charges a flat price per page, however long it is.

[Get API key](https://dashboard.grabber.sh/signup) · [All comparisons](https://grabber.sh/alternatives)

## At a glance

|  | Grabber | Jina Reader |
| --- | --- | --- |
| How you pay | Prepaid credit, from $5. No plans | Prepaid token packs: $50 for 1 billion tokens |
| Pay without a subscription | Yes: it is the only way to pay | Yes: one-off packs, with optional auto top-up |
| A static page | $0.001 | By length: $0.05 per million output tokens on the $50 pack |
| A JavaScript page | $0.005 with js_render=true | The same token price, with the browser engine |
| Failed requests | Never charged. A page that loads with a 404 or 500 status is charged | Not charged |
| Unused credit | Valid for 365 days | Expiry not stated on the pricing page |
| Free tier | $1 at signup, about 1,000 pages, no card | 10 million tokens for non-commercial use; 20 requests a minute without a key |
| Formats | Markdown, HTML, text, links, raw body, page metadata | Markdown, HTML, text, screenshots, JSON extraction with ReaderLM-v2 |
| MCP server | Yes, remote | Yes, remote |
| Rate limit | 60 requests a minute per key | 500 requests a minute with a paid key |

Checked on October 27, 2026 against Jina Reader's own pricing page and docs. Prices change, so check the sources below before you decide.

## What it costs

| Example | Grabber | Jina Reader |
| --- | --- | --- |
| Smallest purchase | $5 | $50 |
| A page of 5,000 output tokens | $0.001 | About $0.00025 |
| A page of 40,000 output tokens | $0.001 | About $0.002 |

Jina prices on the $50 pack. The token count of a page is roughly its length in words, times 1.3.

## When Jina Reader is the better choice

- Your pages are short or medium length. At $0.05 per million tokens, a typical page costs a fraction of $0.001.
- You want to try it without signing up: Reader works without a key, at 20 requests a minute.
- You need screenshots, CSS selectors to keep or remove parts of a page, or forwarded cookies.
- You need very high throughput: premium keys allow 5,000 requests a minute.

## When Grabber is the better choice

- You want to start small: Grabber top-ups start at $5, and Jina's smallest pack is $50.
- You want a price you can predict: every page costs $0.001, however long it is.
- Your free use is commercial: Grabber's signup credit has no non-commercial limit.
- You want links, page metadata and the raw body as separate fields of one response.

## Switching from Jina Reader

Reader puts the target in the path and options in headers. Grabber puts both in the query.

| Jina Reader | Grabber |
| --- | --- |
| `https://r.jina.ai/https://example.com` | `/fetch?url=https://example.com&formats=markdown&unwrap=true` |
| `X-Engine: browser` | `js_render=true` |
| `X-Return-Format: html` | `formats=html` |
| `X-Return-Format: text` | `formats=text` |
| `Authorization: Bearer jina_…` | `Authorization: Bearer gsh_…` |

## Questions

### Is Grabber cheaper than Jina Reader?

Usually not per page: Jina charges by output tokens, and a typical page costs less than $0.001. Grabber costs less to start, $5 instead of $50, and a long page costs the same as a short one.

### Does Jina Reader render JavaScript?

Yes, with its browser engine. Grabber renders a page when you add js_render=true, for $0.005.

### Can I use Jina's free tier in a commercial product?

Jina's free tokens are for non-commercial use. Grabber's signup credit can be used for anything our terms allow.

## Sources

- [Jina Reader](https://jina.ai/reader/)
- [Jina API pricing](https://jina.ai/api-dashboard/pricing/)
- [Reader API docs](https://r.jina.ai/docs)
- [Jina MCP server](https://github.com/jina-ai/MCP)

Grabber's prices are on the [pricing page](https://grabber.sh/pricing) and in the [docs](https://grabber.sh/docs/pricing-and-limits).

## Other comparisons

- [Grabber vs Firecrawl](https://grabber.sh/alternatives/firecrawl): Monthly plans
- [Grabber vs Tavily Extract](https://grabber.sh/alternatives/tavily-extract): Credits
- [Grabber vs ScrapingBee](https://grabber.sh/alternatives/scrapingbee): Monthly plans
- [Grabber vs Scrape.do](https://grabber.sh/alternatives/scrape-do): Monthly plans
- [Grabber vs Apify](https://grabber.sh/alternatives/apify): Compute time
- [Grabber vs ZenRows](https://grabber.sh/alternatives/zenrows): Monthly plans

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/alternatives/jina-reader

---

# Grabber vs Tavily Extract

> Grabber vs Tavily Extract: price per page, plans, free tiers, failed requests, formats and MCP, side by side. Checked October 2026.

Tavily is a search API for agents, and Extract is its endpoint for reading pages, up to 20 at a time. Grabber reads one page per request, in more formats and at a lower price per page.

[Get API key](https://dashboard.grabber.sh/signup) · [All comparisons](https://grabber.sh/alternatives)

## At a glance

|  | Grabber | Tavily Extract |
| --- | --- | --- |
| How you pay | Prepaid credit, from $5. No plans | Credits: a free plan, pay as you go at $0.008 a credit, or monthly plans from $30 |
| Pay without a subscription | Yes: it is the only way to pay | Yes |
| A static page | $0.001 | About $0.0016 pay as you go: 5 extractions per credit |
| A JavaScript page | $0.005 with js_render=true | About $0.0032 with advanced extraction |
| Failed requests | Never charged. A page that loads with a 404 or 500 status is charged | Not charged |
| Unused credit | Valid for 365 days | Credits reset on the first of each month |
| Free tier | $1 at signup, about 1,000 pages, no card | 1,000 credits a month, about 5,000 extractions, no card |
| Formats | Markdown, HTML, text, links, raw body, page metadata | Markdown or text, with optional images |
| MCP server | Yes, remote | Yes, remote and local |
| Rate limit | 60 requests a minute per key | 100 a minute on development keys, 1,000 on production keys |

Checked on October 27, 2026 against Tavily Extract's own pricing page and docs. Prices change, so check the sources below before you decide.

## What it costs

| Example | Grabber | Tavily Extract |
| --- | --- | --- |
| 1,000 pages a month | $1 | $0 on the free plan |
| 10,000 pages a month | $10 | $16 pay as you go |
| 100,000 pages a month | $100 | $160 pay as you go |

Basic extraction. Tavily plans lower the price per credit: Project is $30 a month for 4,000 credits, about 20,000 extractions.

## When Tavily Extract is the better choice

- You also need web search. Tavily's search and extraction share one key and one balance.
- You read up to about 5,000 pages a month: Tavily's free plan covers that every month.
- Most of your pages need JavaScript: advanced extraction costs about $0.0032 a page, against $0.005 on Grabber.
- You want up to 20 URLs in one call, or page chunks ranked against a query.

## When Grabber is the better choice

- Price per page: $0.001 on Grabber, against about $0.0016 on Tavily pay as you go.
- You need HTML, links, the raw body or page metadata. Tavily Extract returns Markdown or text.
- Credit that lasts: Grabber credit is valid for 365 days, and Tavily plan credits reset every month.
- Every request has one price, $0.001. Tavily prices basic extraction as 1 credit per 5 URLs, and its docs do not say how a single URL is billed.

## Switching from Tavily Extract

Tavily Extract takes a JSON body with a list of URLs. Grabber reads one URL per GET request.

| Tavily Extract | Grabber |
| --- | --- |
| `POST /extract` with `urls` | `GET /fetch?url=…`, one URL per request |
| `format: "markdown"` | `formats=markdown` |
| `format: "text"` | `formats=text` |
| `extract_depth: "advanced"` | `js_render=true` |

## Questions

### Is Grabber cheaper than Tavily Extract?

Per page, yes: $0.001 against about $0.0016 on pay as you go. For pages that need JavaScript, Tavily's advanced extraction costs less. Under about 5,000 pages a month, Tavily's free plan costs nothing.

### Does Grabber search the web?

No. Grabber reads pages you already have the address of. Pair it with any search API.

### Can Grabber read many URLs in one call?

No, one per request. Send requests in parallel, up to 60 a minute per key.

## Sources

- [Tavily pricing](https://www.tavily.com/pricing)
- [Tavily API credits](https://docs.tavily.com/documentation/api-credits)
- [Tavily Extract reference](https://docs.tavily.com/documentation/api-reference/endpoint/extract)
- [Tavily rate limits](https://docs.tavily.com/documentation/rate-limits)
- [Tavily MCP server](https://docs.tavily.com/documentation/mcp)

Grabber's prices are on the [pricing page](https://grabber.sh/pricing) and in the [docs](https://grabber.sh/docs/pricing-and-limits).

## Other comparisons

- [Grabber vs Firecrawl](https://grabber.sh/alternatives/firecrawl): Monthly plans
- [Grabber vs Jina Reader](https://grabber.sh/alternatives/jina-reader): Prepaid tokens
- [Grabber vs ScrapingBee](https://grabber.sh/alternatives/scrapingbee): Monthly plans
- [Grabber vs Scrape.do](https://grabber.sh/alternatives/scrape-do): Monthly plans
- [Grabber vs Apify](https://grabber.sh/alternatives/apify): Compute time
- [Grabber vs ZenRows](https://grabber.sh/alternatives/zenrows): Monthly plans

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/alternatives/tavily-extract

---

# Grabber vs ScrapingBee

> Grabber vs ScrapingBee: price per page, plans, free tiers, failed requests, formats and MCP, side by side. Checked October 2026.

ScrapingBee is a scraping API with rotating proxies and rendering on by default, on monthly plans from $19. Grabber has no plan: you pay $0.001 per page, only when you use it.

[Get API key](https://dashboard.grabber.sh/signup) · [All comparisons](https://grabber.sh/alternatives)

## At a glance

|  | Grabber | ScrapingBee |
| --- | --- | --- |
| How you pay | Prepaid credit, from $5. No plans | Monthly plans: Hobby $19 for 75,000 credits, Freelance $49 for 250,000 |
| Pay without a subscription | Yes: it is the only way to pay | No |
| A static page | $0.001 | 1 credit with render_js=false: about $0.00025 on Hobby |
| A JavaScript page | $0.005 with js_render=true | 5 credits, the default: about $0.0013 on Hobby |
| Failed requests | Never charged. A page that loads with a 404 or 500 status is charged | Not charged. A page with a 404 or 410 status is charged |
| Unused credit | Valid for 365 days | Credits are valid for one month |
| Free tier | $1 at signup, about 1,000 pages, no card | 1,000 credits once, no card |
| Formats | Markdown, HTML, text, links, raw body, page metadata | HTML, Markdown, text, JSON, screenshots, CSS and AI extraction |
| MCP server | Yes, remote | Yes, remote |
| Rate limit | 60 requests a minute per key | 25 concurrent requests on Hobby |

Checked on October 27, 2026 against ScrapingBee's own pricing page and docs. Prices change, so check the sources below before you decide.

## What it costs

| Example | Grabber | ScrapingBee |
| --- | --- | --- |
| 1,000 pages a month | $1 | $19: Hobby |
| 10,000 pages a month | $10 | $19: Hobby |
| 100,000 pages a month | $100 | $49: Freelance |

Static pages with render_js=false. With rendering, the default, each page uses 5 credits.

## When ScrapingBee is the better choice

- Sites you need block plain requests. Premium and stealth proxies, geotargeting and an auto mode are built for that, and Grabber has none of them.
- You fetch more than about 19,000 static pages every month: the Hobby plan then costs less per page.
- You need screenshots, clicks and scrolls, or ready-made APIs for Google and Amazon.

## When Grabber is the better choice

- You fetch fewer than about 19,000 pages a month, or your volume varies: you pay for what you fetch, and credit lasts 365 days instead of one month.
- You want no subscription to manage: no plan to pick, upgrade or renew.
- You want a short price list: two prices, and no credit multipliers to work out.

## Switching from ScrapingBee

Both are GET APIs. Grabber reads the key from a header instead of the query.

| ScrapingBee | Grabber |
| --- | --- |
| `api_key=` in the query | `Authorization: Bearer gsh_…` header |
| `render_js=false` | The default: no parameter |
| `render_js=true`, the default | `js_render=true` |
| `return_page_markdown=true` | `formats=markdown&unwrap=true` |
| `return_page_text=true` | `formats=text&unwrap=true` |

## Questions

### Is Grabber cheaper than ScrapingBee?

Below about 19,000 static pages a month, yes, because ScrapingBee's cheapest plan is $19 a month and Grabber charges per page. Above that, a ScrapingBee plan you use in full costs less per page.

### Does Grabber use proxies or get past anti-bot systems?

No. Grabber loads public pages directly. It does not rotate proxies or solve CAPTCHAs. If the sites you need block automated requests, a scraping API with proxies is the better tool.

### Does ScrapingBee have an MCP server?

Yes, a remote one. Grabber has one too, at mcp.grabber.sh.

## Sources

- [ScrapingBee pricing](https://www.scrapingbee.com/pricing/)
- [ScrapingBee documentation](https://www.scrapingbee.com/documentation/)
- [ScrapingBee FAQ](https://www.scrapingbee.com/faq/)
- [ScrapingBee remote MCP](https://www.scrapingbee.com/documentation/remote-mcp/)

Grabber's prices are on the [pricing page](https://grabber.sh/pricing) and in the [docs](https://grabber.sh/docs/pricing-and-limits).

## Other comparisons

- [Grabber vs Firecrawl](https://grabber.sh/alternatives/firecrawl): Monthly plans
- [Grabber vs Jina Reader](https://grabber.sh/alternatives/jina-reader): Prepaid tokens
- [Grabber vs Tavily Extract](https://grabber.sh/alternatives/tavily-extract): Credits
- [Grabber vs Scrape.do](https://grabber.sh/alternatives/scrape-do): Monthly plans
- [Grabber vs Apify](https://grabber.sh/alternatives/apify): Compute time
- [Grabber vs ZenRows](https://grabber.sh/alternatives/zenrows): Monthly plans

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/alternatives/scrapingbee

---

# Grabber vs Scrape.do

> Grabber vs Scrape.do: price per page, plans, free tiers, failed requests, formats and MCP, side by side. Checked October 2026.

Scrape.do is a proxy-based scraping API built to get past anti-bot systems, on monthly plans from $29. Grabber is a pay-per-page API for public pages, with no plan.

[Get API key](https://dashboard.grabber.sh/signup) · [All comparisons](https://grabber.sh/alternatives)

## At a glance

|  | Grabber | Scrape.do |
| --- | --- | --- |
| How you pay | Prepaid credit, from $5. No plans | Monthly plans: Hobby $29 for 250,000 credits, Pro $99 for 1,250,000 |
| Pay without a subscription | Yes: it is the only way to pay | No |
| A static page | $0.001 | 1 credit: about $0.00012 on Hobby |
| A JavaScript page | $0.005 with js_render=true | 5 credits with render=true: about $0.00058 on Hobby |
| Failed requests | Never charged. A page that loads with a 404 or 500 status is charged | Not charged. Its docs differ on whether 400, 404 and 410 pages are charged |
| Unused credit | Valid for 365 days | Not stated |
| Free tier | $1 at signup, about 1,000 pages, no card | 1,000 credits a month, no card |
| Formats | Markdown, HTML, text, links, raw body, page metadata | The raw page, Markdown, screenshots |
| MCP server | Yes, remote | None in its docs; only third-party servers |
| Rate limit | 60 requests a minute per key | 10 concurrent requests on Hobby |

Checked on October 27, 2026 against Scrape.do's own pricing page and docs. Prices change, so check the sources below before you decide.

## What it costs

| Example | Grabber | Scrape.do |
| --- | --- | --- |
| 1,000 pages a month | $1 | $0 on the free plan |
| 10,000 pages a month | $10 | $29: Hobby |
| 100,000 pages a month | $100 | $29: Hobby |

Static pages on datacenter proxies. Rendering uses 5 credits, residential proxies 10.

## When Scrape.do is the better choice

- Sites you need block automated requests. Residential and mobile proxies, geotargeting and anti-bot handling are what Scrape.do is built for.
- You fetch more than about 29,000 pages every month: Hobby then costs much less per page.
- You need sticky sessions, browser interactions or ZIP-code targeting.

## When Grabber is the better choice

- You fetch fewer than about 29,000 pages a month, or not every month: there is no $29 plan, and credit lasts 365 days.
- Your agent uses MCP: Grabber has a remote MCP server, and Scrape.do's docs list none, only third-party ones.
- You need text, links or page metadata as separate outputs, not only the page or its Markdown.

## Switching from Scrape.do

Both are GET APIs. Grabber reads the key from a header instead of the query.

| Scrape.do | Grabber |
| --- | --- |
| `token=` in the query | `Authorization: Bearer gsh_…` header |
| `render=true` | `js_render=true` |
| `output=markdown` | `formats=markdown&unwrap=true` |
| `super=true` | No equivalent: Grabber has no residential proxies |

## Questions

### Is Grabber cheaper than Scrape.do?

Below about 29,000 pages a month, yes: Scrape.do's cheapest plan is $29 a month. Above that, Scrape.do costs much less per page, and its free plan covers 1,000 pages every month.

### Does Grabber get past anti-bot systems?

No. Grabber loads public pages directly, without proxies or CAPTCHA solving. For sites that block automated requests, Scrape.do is the better tool.

### Does Scrape.do have an MCP server?

Its docs list none, as of October 2026; only third-party servers exist. Grabber has a remote MCP server at mcp.grabber.sh.

## Sources

- [Scrape.do pricing](https://scrape.do/pricing/)
- [Scrape.do documentation](https://scrape.do/documentation/)
- [Scrape.do request costs](https://scrape.do/documentation/request-costs/)
- [Scrape.do response output](https://scrape.do/documentation/api-response/response-output/)

Grabber's prices are on the [pricing page](https://grabber.sh/pricing) and in the [docs](https://grabber.sh/docs/pricing-and-limits).

## Other comparisons

- [Grabber vs Firecrawl](https://grabber.sh/alternatives/firecrawl): Monthly plans
- [Grabber vs Jina Reader](https://grabber.sh/alternatives/jina-reader): Prepaid tokens
- [Grabber vs Tavily Extract](https://grabber.sh/alternatives/tavily-extract): Credits
- [Grabber vs ScrapingBee](https://grabber.sh/alternatives/scrapingbee): Monthly plans
- [Grabber vs Apify](https://grabber.sh/alternatives/apify): Compute time
- [Grabber vs ZenRows](https://grabber.sh/alternatives/zenrows): Monthly plans

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/alternatives/scrape-do

---

# Grabber vs Apify

> Grabber vs Apify Website Content Crawler: price per page, plans, free tiers, failed requests, formats and MCP, side by side. Checked October 2026.

Apify's Website Content Crawler crawls whole sites into Markdown for RAG, billed by compute time on Apify's platform. Grabber fetches one page per request, at a flat price.

[Get API key](https://dashboard.grabber.sh/signup) · [All comparisons](https://grabber.sh/alternatives)

## At a glance

|  | Grabber | Apify |
| --- | --- | --- |
| How you pay | Prepaid credit, from $5. No plans | Platform plans: Free with $5 of usage a month, Starter $19 a month |
| Pay without a subscription | Yes: it is the only way to pay | Only as overage on a paid plan |
| A static page | $0.001 | About $0.0002 with the raw HTTP crawler, by Apify's estimate |
| A JavaScript page | $0.005 with js_render=true | $0.0005 to $0.005 with a headless browser, by Apify's estimate |
| Failed requests | Never charged. A page that loads with a 404 or 500 status is charged | Not stated. Usage is billed by compute time, including retries |
| Unused credit | Valid for 365 days | Monthly usage does not roll over |
| Free tier | $1 at signup, about 1,000 pages, no card | $5 of usage a month, no card |
| Formats | Markdown, HTML, text, links, raw body, page metadata | Markdown, text, HTML, metadata, files, screenshots |
| MCP server | Yes, remote | Yes, through Apify's MCP server |
| Rate limit | 60 requests a minute per key | 5 concurrent runs on the free plan |

Checked on October 27, 2026 against Apify's own pricing page and docs. Prices change, so check the sources below before you decide.

## What it costs

| Example | Grabber | Apify |
| --- | --- | --- |
| 1,000 static pages | $1 | About $0.20, inside the free $5 |
| 100,000 static pages | $100 | About $20 |
| 1,000 rendered pages | $5 | $0.50 to $5 |

Apify's figures are the estimates on its Actor page, which assume $0.25 per compute unit; its pricing page lists $0.20 or less. The real cost depends on the site.

## When Apify is the better choice

- You want to crawl whole sites: depth and page limits, URL patterns, sitemaps, llms.txt and incremental crawls.
- You want a platform around the crawler: schedules, webhooks, storage, and integrations with LangChain, LlamaIndex and vector databases.
- On static sites at volume, Apify's estimated cost per page is lower.
- You need sign-in with cookies, proxies, or removal of cookie banners.

## When Grabber is the better choice

- You fetch single pages: one GET request returns the page, instead of a crawler run. Apify itself suggests another tool for single URLs.
- You want to know the price before the request: $0.001 a page, not compute time that depends on the site.
- Your agent fetches pages as it works: Grabber's fetch_page tool returns the content in the tool result.
- Credit that lasts a year, with no monthly plan.

## Switching from Apify

Website Content Crawler runs as an Actor and writes to a dataset. Grabber answers each GET request with the page.

| Apify | Grabber |
| --- | --- |
| Run the Actor with `startUrls` | `GET /fetch?url=…`, one page per request |
| `crawlerType: "cheerio"` | The default |
| `crawlerType: "playwright:firefox"` | `js_render=true` |
| Markdown in the dataset | `formats=markdown` |
| `maxCrawlDepth` and URL globs | No equivalent: follow `formats=links` yourself |

## Questions

### Is Grabber cheaper than Apify?

For static pages at volume, Apify's own estimate of about $0.20 per 1,000 pages is lower than Grabber's $1. For single pages on demand, Grabber is simpler: $0.001 a page, with no plan and no compute time.

### Can Grabber crawl a whole site?

No. Grabber fetches one page per request. Ask for formats=links to see where to go next.

### Does Apify have an MCP server?

Yes, at mcp.apify.com, and it can run Website Content Crawler. Grabber's MCP server returns the page in the tool result.

## Sources

- [Website Content Crawler](https://apify.com/apify/website-content-crawler)
- [Apify pricing](https://apify.com/pricing)
- [Apify billing docs](https://docs.apify.com/platform/console/billing)
- [Apify MCP server](https://docs.apify.com/platform/integrations/mcp)

Grabber's prices are on the [pricing page](https://grabber.sh/pricing) and in the [docs](https://grabber.sh/docs/pricing-and-limits).

## Other comparisons

- [Grabber vs Firecrawl](https://grabber.sh/alternatives/firecrawl): Monthly plans
- [Grabber vs Jina Reader](https://grabber.sh/alternatives/jina-reader): Prepaid tokens
- [Grabber vs Tavily Extract](https://grabber.sh/alternatives/tavily-extract): Credits
- [Grabber vs ScrapingBee](https://grabber.sh/alternatives/scrapingbee): Monthly plans
- [Grabber vs Scrape.do](https://grabber.sh/alternatives/scrape-do): Monthly plans
- [Grabber vs ZenRows](https://grabber.sh/alternatives/zenrows): Monthly plans

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/alternatives/apify

---

# Grabber vs ZenRows

> Grabber vs ZenRows: price per page, plans, free tiers, failed requests, formats and MCP, side by side. Checked October 2026.

ZenRows Fetch, formerly the Universal Scraper API, reads pages behind anti-bot systems, on monthly plans from $19. Grabber charges $0.001 per page, with no plan.

[Get API key](https://dashboard.grabber.sh/signup) · [All comparisons](https://grabber.sh/alternatives)

## At a glance

|  | Grabber | ZenRows |
| --- | --- | --- |
| How you pay | Prepaid credit, from $5. No plans | Monthly plans: Build from $19 for 45,000 credits |
| Pay without a subscription | Yes: it is the only way to pay | No, except for AI agents buying through its agent storefront |
| A static page | $0.001 | 1 credit: about $0.00042 on Build |
| A JavaScript page | $0.005 with js_render=true | 5 credits with js_render=true: about $0.0021 on Build |
| Failed requests | Never charged. A page that loads with a 404 or 500 status is charged | Not charged. A page with a 404 or 410 status is charged |
| Unused credit | Valid for 365 days | Plan credits expire at each renewal |
| Free tier | $1 at signup, about 1,000 pages, no card | 5,000 credits a month, no card |
| Formats | Markdown, HTML, text, links, raw body, page metadata | HTML, Markdown, text, PDF, screenshots, structured extraction |
| MCP server | Yes, remote | Yes, remote and local |
| Rate limit | 60 requests a minute per key | 20 concurrent requests on Build |

Checked on October 27, 2026 against ZenRows's own pricing page and docs. Prices change, so check the sources below before you decide.

## What it costs

| Example | Grabber | ZenRows |
| --- | --- | --- |
| 1,000 pages a month | $1 | $0 on the free plan |
| 10,000 pages a month | $10 | $19: Build |
| 100,000 pages a month | $100 | $39: Build with 120,000 credits |

Monthly billing, static pages. Rendering uses 5 credits, premium proxies 10.

## When ZenRows is the better choice

- Sites you need block automated requests: premium proxies, an adaptive stealth mode and 190+ countries.
- You fetch more than about 19,000 pages every month: a Build plan then costs less per page.
- You read up to 5,000 pages a month: the free plan covers that every month.
- You need page interactions, sticky sessions, screenshots or PDFs of pages.

## When Grabber is the better choice

- You fetch fewer than about 19,000 pages a month, or your volume varies: no plan, and credit lasts 365 days instead of expiring at renewal.
- You want a short price list: two prices, and no credit multipliers to work out.
- You want page metadata as fields: title, Open Graph tags, JSON-LD, emails and phone numbers, with includeMetadata=true.

## Switching from ZenRows

Both are GET APIs, and js_render even has the same name. Grabber reads the key from a header.

| ZenRows | Grabber |
| --- | --- |
| `apikey=` in the query | `Authorization: Bearer gsh_…` header |
| `js_render=true` | `js_render=true` |
| `response_type=markdown` | `formats=markdown&unwrap=true` |
| `premium_proxy=true` | No equivalent: Grabber has no premium proxies |

## Questions

### Is Grabber cheaper than ZenRows?

Below about 19,000 pages a month, yes: ZenRows' paid plans start at $19 a month. Above that, a ZenRows plan you use in full costs less per page, and up to 5,000 pages a month are free.

### Does Grabber get past anti-bot systems?

No. Grabber loads public pages directly, without proxies or CAPTCHA solving. For sites that block automated requests, ZenRows is the better tool.

### Do both have an MCP server?

Yes. ZenRows has remote and local servers. Grabber has a remote server at mcp.grabber.sh.

## Sources

- [ZenRows pricing docs](https://docs.zenrows.com/first-steps/pricing)
- [ZenRows pricing](https://www.zenrows.com/pricing)
- [ZenRows Fetch reference](https://docs.zenrows.com/fetch/api-reference)
- [ZenRows MCP server](https://docs.zenrows.com/mcp/overview)

Grabber's prices are on the [pricing page](https://grabber.sh/pricing) and in the [docs](https://grabber.sh/docs/pricing-and-limits).

## Other comparisons

- [Grabber vs Firecrawl](https://grabber.sh/alternatives/firecrawl): Monthly plans
- [Grabber vs Jina Reader](https://grabber.sh/alternatives/jina-reader): Prepaid tokens
- [Grabber vs Tavily Extract](https://grabber.sh/alternatives/tavily-extract): Credits
- [Grabber vs ScrapingBee](https://grabber.sh/alternatives/scrapingbee): Monthly plans
- [Grabber vs Scrape.do](https://grabber.sh/alternatives/scrape-do): Monthly plans
- [Grabber vs Apify](https://grabber.sh/alternatives/apify): Compute time

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/alternatives/zenrows

---

# The Grabber blog

> Guides for giving AI agents clean web data: Markdown for RAG, when a page needs JavaScript rendering, MCP setup, and what each one costs.

How to get web pages into models: clean formats, chunking, rendering and tools, with the code and the cost of each step.

- [Give Claude and Cursor Web Access With an MCP Server](https://grabber.sh/blog/mcp-web-access-for-claude-and-cursor): Connect Cursor, Claude Code, Claude Desktop, VS Code or Windsurf to the web with Grabber's remote MCP server. Setup, tools, prompts and cost. (October 27, 2026)
- [Static Fetch vs JavaScript Rendering: When You Need a Headless Browser](https://grabber.sh/blog/static-fetch-vs-javascript-rendering): Most pages don't need a headless browser. How to tell when one does, what each costs, and a static-first fallback that cuts the bill. (October 27, 2026)
- [Turn Any Web Page Into Clean Markdown for RAG](https://grabber.sh/blog/web-page-to-markdown-for-rag): Why Markdown beats raw HTML for retrieval, how to fetch any URL as Markdown, and how to chunk it by headings in Python and Node.js. (October 27, 2026)

## Give your agents the web

Sign up in a minute and fetch your first 1,000 pages for free.

[Get API key](https://dashboard.grabber.sh/signup) · [Read the docs](https://grabber.sh/docs)

Source: https://grabber.sh/blog

---

# Give Claude and Cursor Web Access With an MCP Server

> Connect Cursor, Claude Code, Claude Desktop, VS Code or Windsurf to the web with Grabber's remote MCP server. Setup, tools, prompts and cost.

By Grabber Team · October 27, 2026 · 7 min read

An MCP server gives an AI app new tools. Add Grabber's remote server, `https://mcp.grabber.sh`, to Cursor, Claude Code, Claude Desktop, VS Code or Windsurf, and your agent gets a `fetch_page` tool that loads any public URL and returns it as Markdown, plus a free `check_balance` tool. Setup is one URL and one header in a config file. There is nothing to install, and a page costs $0.001.

## What MCP is

The Model Context Protocol is an open standard, introduced by Anthropic in November 2024, for connecting AI applications to tools and data. The app, such as Claude, Cursor or VS Code, is the client. A server publishes a list of tools, each with a name, a description and the arguments it takes. When the model decides a tool would help, the client calls it on the server and hands the result back to the model.

![The agent asks mcp.grabber.sh for its tools, calls fetch_page with a URL, and the server loads the page and returns Markdown.](https://grabber.sh/images/blog/mcp-fetch-page-sequence.svg "What happens when you ask your agent about a page.")

Servers come in two kinds:

| | Local server | Remote server |
| --- | --- | --- |
| Runs | On your machine, started by the client | On the provider's servers |
| Transport | stdio | Streamable HTTP |
| Install | A package, often run with `npx` or `uvx` | Nothing: a URL |
| Updates | When you update the package | When the provider deploys |
| Credentials | Environment variables | A header, usually a Bearer token |

Grabber's server is remote. Every client connects to the same URL and sends your API key as `Authorization: Bearer gsh_...`.

## Why give a coding agent the web?

A model knows what was in its training data, up to a cutoff date. Anything newer, or anything it never saw, it has to guess. Web access turns guessing into reading:

- **Current docs.** "Read the migration guide for version 5 before you change the config" gets you code for the version you use, not the one the model remembers.
- **The actual issue.** Paste the link to a GitHub issue or a changelog and the agent reads it, instead of you copying it into the chat.
- **Research.** Compare three libraries from their docs, or summarize a section of a spec.
- **Your own public pages.** Check that the pricing page shows the new price after a deploy.

The agent gets Markdown instead of HTML, so a docs page costs far fewer tokens than its markup would, and more of the context window is left for your code.

## Set it up

First create an API key in the dashboard ([how keys work](https://grabber.sh/docs/authentication)). Keys start with `gsh_` and are shown only once, so copy yours when you create it. New accounts get $1 of free credit, about 1,000 pages. Then add the server to your client.

### Cursor

Put this in `~/.cursor/mcp.json` to use it in every project, or in `.cursor/mcp.json` inside one project:

```json
{
  "mcpServers": {
    "grabber": {
      "url": "https://mcp.grabber.sh",
      "headers": {
        "Authorization": "Bearer gsh_your_api_key"
      }
    }
  }
}
```

The server and its two tools then show up in Cursor's MCP settings. If they do not, reload the window.

### Claude Code

One command in your terminal:

```bash
claude mcp add --transport http grabber https://mcp.grabber.sh \
  --header "Authorization: Bearer gsh_your_api_key"
```

By default the server is added for the current project. Add `--scope user` to have it in every project, and run `claude mcp list` to check the connection.

### VS Code

Add `.vscode/mcp.json` to your project:

```json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "grabber-key",
      "description": "Grabber API key",
      "password": true
    }
  ],
  "servers": {
    "grabber": {
      "type": "http",
      "url": "https://mcp.grabber.sh",
      "headers": {
        "Authorization": "Bearer ${input:grabber-key}"
      }
    }
  }
}
```

VS Code asks for the key the first time the server starts and stores it for you, so the key never lands in the file and the file is safe to commit.

### Windsurf

Windsurf reads `~/.codeium/windsurf/mcp_config.json`, and calls the address `serverUrl`:

```json
{
  "mcpServers": {
    "grabber": {
      "serverUrl": "https://mcp.grabber.sh",
      "headers": {
        "Authorization": "Bearer gsh_your_api_key"
      }
    }
  }
}
```

### Claude Desktop

Claude Desktop's config file starts local servers, so it reaches a remote one through `mcp-remote`, a small bridge that runs with `npx` and needs Node.js. Edit `claude_desktop_config.json` (Settings, then Developer, then Edit Config) and restart the app:

```json
{
  "mcpServers": {
    "grabber": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://mcp.grabber.sh",
        "--header",
        "Authorization:${GRABBER_AUTH}"
      ],
      "env": {
        "GRABBER_AUTH": "Bearer gsh_your_api_key"
      }
    }
  }
}
```

The header value comes from the `GRABBER_AUTH` variable, which keeps the space in "Bearer gsh_..." intact on every operating system.

## The two tools

| Tool | Arguments | Returns | Cost |
| --- | --- | --- | --- |
| `fetch_page` | `url` (required), `format`, `js_render`, `max_chars` | The page as Markdown, or as HTML, text or links | $0.001, or $0.005 with `js_render`. Errors are free. |
| `check_balance` | None | Your credit, in US dollars | Free |

`format` defaults to `markdown`; the other choices are `html`, `text` and `links`. `max_chars` cuts the content after that many characters, up to 100,000, so one long page cannot fill the context window. A page that was cut says so at the end, and costs the same as a whole one.

Everything else works like the API: the same prices, the same limit of 60 requests a minute per key (shared with your API calls), and every call shows up in the dashboard's request log with its cost.

## Prompts that work

Agents call a tool when the task clearly needs it, so be explicit:

- "Read https://nextjs.org/docs/app/guides/upgrading/version-16 and list the changes that affect this project."
- "Fetch the README at https://github.com/modelcontextprotocol/typescript-sdk and set up a client the way it shows."
- "Compare the pricing pages of these three services in a table."
- "How much Grabber credit do I have left?" This one calls `check_balance`, which is free.

For a project you work on every day, put the rule in your agent's instructions (`AGENTS.md`, `CLAUDE.md` or a Cursor rule), so you do not have to repeat it:

```markdown
When you need a web page, call the grabber fetch_page tool instead of guessing.
Use js_render only if the page comes back empty or asks for JavaScript.
Pass max_chars: 20000 unless you need the whole page.
```

## Cost, keys and safety

- **Cost.** Each `fetch_page` call is one request: $0.001, or $0.005 rendered. An agent that reads 50 pages during a task spends 5 cents. Failed fetches and `check_balance` are free.
- **One key per machine.** Create a separate key for each laptop or CI runner, so you can delete one in the dashboard without breaking the others.
- **Keep keys out of git.** `~/.cursor/mcp.json` lives in your home directory, but a project's `.cursor/mcp.json` is easy to commit by accident. Use VS Code's input prompt, or keep keys out of project files.
- **Treat pages as untrusted input.** A fetched page is text written by someone else, and it can contain instructions aimed at your agent. `fetch_page` only reads, but your agent may also have tools that write files or run commands, so keep approval prompts on for those.

## Troubleshooting

| What you see | What to do |
| --- | --- |
| The server fails to connect, or answers 401 | The key is missing or wrong. The header must be exactly `Authorization: Bearer gsh_...`, with one space. |
| The agent never calls the tools | Ask for them by name ("use fetch_page"), and check the server is turned on in the client's MCP settings. |
| The tool says your credit is too low | Add credit in the dashboard, or turn on auto top-up. |
| The tool says you are rate limited | You made more than 60 calls this minute on this key. Wait for the next minute. |
| `npx` is not found (Claude Desktop) | Install Node.js, which `mcp-remote` needs. |
| A page comes back nearly empty | It is built with JavaScript. Ask the agent to call `fetch_page` again with `js_render: true`. |

## FAQ

### Do I need to install anything?

Not for Cursor, Claude Code, VS Code or Windsurf: they connect to the URL directly. Claude Desktop needs Node.js for the `mcp-remote` bridge.

### Is there a local version of the server?

No. The server is remote only, so there is nothing to install or keep up to date.

### Does it work with other MCP clients?

Any client that can connect to a remote server over Streamable HTTP and send a custom header can use it. Clients that only start local servers can go through `mcp-remote`, as Claude Desktop does above.

### Can my agent use the REST API instead?

Yes, if it can make HTTP requests. The [docs](https://grabber.sh/docs) and the [OpenAPI file](https://grabber.sh/openapi.json) describe the API. MCP is simply the easiest way to give a chat or coding agent the tool.

Source: https://grabber.sh/blog/mcp-web-access-for-claude-and-cursor

---

# Static Fetch vs JavaScript Rendering: When You Need a Headless Browser

> Most pages don't need a headless browser. How to tell when one does, what each costs, and a static-first fallback that cuts the bill.

By Grabber Team · October 27, 2026 · 7 min read

Most web pages do not need a headless browser. A static fetch, which is one HTTP request for the HTML the server sends, gets the full content of server-rendered sites: most documentation, news sites, blogs and shops. You need JavaScript rendering when the server sends an empty shell and the browser builds the page with scripts. The practical rule is to fetch statically first, check whether the content came back, and render only the pages that need it. With Grabber, that means $0.001 for most pages and $0.005 only for the rest.

![Fetch statically first. If the content is in the Markdown, use it. If not, check the raw HTML for JSON-LD or framework data. Render with js_render=true only when neither has it.](https://grabber.sh/images/blog/static-or-rendered.svg "Static first: render only when the content is missing from both the Markdown and the raw HTML.")

## What a static fetch sees

When a browser opens a page, it does two things. It downloads the HTML, then it runs the page's JavaScript, which may load more data and change what is on screen. A static fetch stops after the first step. A rendered fetch does both, in a real browser without a window, and reads the page after its scripts have run.

For a server-rendered page, the two give the same text, because the server already put it in the HTML. For a single-page app, the HTML the server sends looks like this:

```html
<!doctype html>
<html lang="en">
  <head>
    <title>Acme Dashboard</title>
    <script type="module" src="/assets/index-4f2c1a.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <noscript>You need to enable JavaScript to run this app.</noscript>
  </body>
</html>
```

Everything a visitor sees is drawn into that empty `<div id="root">` by the script. A static fetch of this page gets a title and the `noscript` notice, and its Markdown is nearly empty.

## Which pages need rendering?

You cannot always tell from the outside, but how a site is built is a strong hint:

| Kind of site | Content in the HTML? | Needs `js_render`? |
| --- | --- | --- |
| Documentation (Docusaurus, MkDocs, Sphinx, most docs platforms) | Yes, built ahead of time | Rarely |
| News sites, blogs, WordPress, Ghost | Yes, rendered on the server | Rarely |
| Next.js, Nuxt, Astro and SvelteKit sites | Usually, rendered on the server or built ahead | Sometimes, for parts that load later |
| Shops | The product text usually; prices and stock sometimes load later | Sometimes |
| Single-page apps (React, Vue or Angular, built with Vite or similar) | No, an empty shell | Yes |
| Lists that grow when you click "Load more" | The first items at most | Only what loads without a click |

The last row is a limit of rendering, not of static fetches. A headless browser runs the scripts that build the page, but it does not press buttons or sign in, so content behind a click or a login stays out of reach either way.

## How to tell from the response

You do not have to guess site by site. Fetch the page statically and look at what came back. Any of these means it probably needs rendering:

- **The Markdown is nearly empty**, or holds only the menu and the footer.
- **It contains a "JavaScript required" notice**, like "You need to enable JavaScript to run this app".
- **It says "Loading…"** where the content should be.
- **The thing you came for is missing**: the article body, the price, the table you saw in your browser.

Before you switch to rendering, check one more place. Many sites that build their pages in the browser still ship the data inside the HTML, for search engines or to start the app:

- **JSON-LD.** Product pages, articles and recipes often describe themselves in a `<script type="application/ld+json">` tag. Add `includeMetadata=true` and Grabber returns it already parsed, in `data.metadata.jsonLd`.
- **Framework data.** Next.js pages built with the Pages Router put their props in `<script id="__NEXT_DATA__">`, and Nuxt uses `__NUXT_DATA__` or `window.__NUXT__`. Ask for `formats=raw` to get the original HTML, and read the JSON from it.

Both are static fetches at $0.001, and structured data is often easier to work with than the rendered text anyway.

## Check a site by hand in two minutes

Before you write code for a new source, look at it the way a static fetch does:

1. **View the page source** (Ctrl+U, or Cmd+Option+U on a Mac). That is the HTML the server sent. Search it for a sentence you can see on the page: if it is there, a static fetch gets it too. Do not judge by the Elements panel in DevTools, which shows the page after its scripts have run.
2. **Turn JavaScript off and reload.** In Chrome DevTools, open the command menu (Ctrl+Shift+P, or Cmd+Shift+P), run "Disable JavaScript" and reload. What is left on screen is roughly what a static fetch sees.
3. **Ask Grabber for both.** Fetch the page once as it is and once with `js_render=true`, and compare the length of the Markdown. That is $0.001 plus $0.005, and it usually settles the question for the whole site, because the pages of one site tend to be built the same way.

## Static first, render when needed

Put those checks in code and the decision makes itself. This version fetches statically, renders only when the result looks empty, and remembers which domains needed rendering, so it stops paying for a static attempt there:

```python
import os
from urllib.parse import urlsplit

import requests

API = "https://api.grabber.sh/fetch"
HEADERS = {"Authorization": f"Bearer {os.environ['GRABBER_API_KEY']}"}
JS_NOTICES = ("enable javascript", "javascript is required", "javascript is disabled")
needs_js = set()


def grab(url, js_render=False):
    params = {"url": url, "formats": "markdown", "unwrap": "true"}
    if js_render:
        params["js_render"] = "true"
    response = requests.get(API, params=params, headers=HEADERS, timeout=130)
    response.raise_for_status()
    return response.text


def looks_unrendered(markdown):
    text = markdown.strip().lower()
    return len(text) < 500 or any(notice in text for notice in JS_NOTICES)


def fetch_page(url):
    domain = urlsplit(url).hostname
    if domain not in needs_js:
        markdown = grab(url)
        if not looks_unrendered(markdown):
            return markdown
        needs_js.add(domain)
    return grab(url, js_render=True)
```

Tune `looks_unrendered` to your own pages. A 500-character threshold suits articles and docs, but a page that is short on purpose, like a contact page, would be rendered for nothing. If you know what you are looking for, such as a price, check for that instead of the length.

Mind the timeout too. A rendered fetch waits for the page to finish loading and can take up to 120 seconds, so give your HTTP client at least 130. A static fetch is one request and comes back far sooner.

## What it costs

At Grabber, rendering costs five times as much as a static fetch, $0.005 against $0.001, and it is slower. At volume, the way you choose adds up. Take 100,000 pages a month, of which 15% need rendering:

| Strategy | Static fetches | Rendered fetches | Monthly cost |
| --- | --- | --- | --- |
| Render everything | 0 | 100,000 | $500 |
| Static first, render on failure | 100,000 | 15,000 | $175 |
| Static first, remember domains | About 85,000 | 15,000 | About $160 |
| Static only | 100,000 | 0 | $100, with 15% of pages empty |

Static first costs about a third of rendering everything, and remembering the domains that need rendering trims a little more. The [pricing calculator](https://grabber.sh/pricing#calculator) does this math for your own numbers.

A failed fetch costs nothing in any of these. If a rendered fetch times out or the site cannot be reached, you are not charged.

## What rendering does not fix

Rendering solves one problem: content that scripts build in the browser. It is not a way around these:

- **Logins.** Grabber reads public pages and does not sign in, so a page behind a login returns what a signed-out visitor sees.
- **Bot protection.** Grabber loads pages directly and does not rotate proxies or solve CAPTCHAs, with or without rendering. If the sites you need block automated requests, a scraping API built around proxies is the better tool, and [our comparisons](https://grabber.sh/alternatives) say which.
- **Interaction.** Rendering runs the page. It does not fill in forms or click through tabs.

## FAQ

### Do I pay for rendering if the page fails?

No. Any error, including a rendered fetch that times out, costs nothing. You pay $0.005 only when the rendered page loads.

### Can Grabber decide when to render?

No, you choose per request with `js_render=true`. The static-first function above does the same job on your side, with your own idea of what "empty" means.

### Does rendering change the formats I can ask for?

No. Markdown, HTML, text, links and metadata all work with `js_render=true`, and they describe the page after its scripts have run.

### Is a single-page app a lost cause without rendering?

Not always. Check for JSON-LD with `includeMetadata=true`, and for framework data in the raw HTML, first. When the data is there, a static fetch is cheaper and the data is cleaner.

Source: https://grabber.sh/blog/static-fetch-vs-javascript-rendering

---

# Turn Any Web Page Into Clean Markdown for RAG

> Why Markdown beats raw HTML for retrieval, how to fetch any URL as Markdown, and how to chunk it by headings in Python and Node.js.

By Grabber Team · October 27, 2026 · 9 min read

To put a web page into a RAG pipeline, fetch it as Markdown instead of HTML, split it on its headings into chunks of a few hundred tokens, and store every chunk with the URL it came from. Markdown keeps what a model uses (headings, lists, tables, links and code) and drops what it does not (menus, scripts, styles and tracking tags). The page gets smaller, the chunks follow the author's own structure, and every answer can cite its source.

This guide walks through each step with code in Python and Node.js. It uses Grabber for the fetching, but the chunking and storage parts work with Markdown from anywhere.

![A web page goes through four steps: fetch as Markdown, split on headings, store each chunk with its source, then retrieve and answer.](https://grabber.sh/images/blog/markdown-rag-pipeline.svg "From URL to answer. The bar lengths are illustrative: measure your own pages with the script below.")

## Why not embed the HTML?

A web page is mostly not content. The HTML a server sends carries the navigation, the footer, cookie banners, inline scripts, style attributes and analytics tags, and the article sits somewhere in the middle. Feed that to an embedding model and three things go wrong:

- **You pay for noise.** Every tag and class name is tokens: once in the embedding call, and again in the prompt each time the chunk is retrieved.
- **Chunks lose their meaning.** A chunk cut at a fixed size can be half menu and half paragraph. Its embedding lands somewhere between the two and matches neither kind of question well.
- **The model reads boilerplate.** Retrieved context full of "Accept all cookies" and "Sign up for our newsletter" leaves less room for the passage that answers the question.

Plain text fixes the noise but throws away the structure. Without headings you cannot tell where one topic ends and the next begins, tables collapse into runs of words, and links lose their targets. Markdown sits in between:

| | Raw HTML | Plain text | Markdown |
| --- | --- | --- | --- |
| Menus, scripts, styles | Kept | Removed | Removed |
| Headings | As tags | Lost | `#`, `##`, `###` |
| Tables | As tags | Flattened | Kept as rows |
| Links | Kept | Lost | `[text](url)` |
| Tokens for the same page | Most | Fewest | Close to plain text |
| Chunk boundaries | Hard to find | Guesswork | The headings |

When a RAG answer is wrong, look at what was retrieved before you change the prompt or the model. Very often the right passage was never in the index in a usable form. Clean input is the cheapest fix there is.

## Fetch a page as Markdown

Grabber has one endpoint. Send the URL and `formats=markdown` with your API key, and the Markdown comes back in a JSON envelope, together with details about the fetch:

```python
import os

import requests

API = "https://api.grabber.sh/fetch"
HEADERS = {"Authorization": f"Bearer {os.environ['GRABBER_API_KEY']}"}


def fetch_markdown(url):
    params = {"url": url, "formats": "markdown"}
    response = requests.get(API, params=params, headers=HEADERS, timeout=130)
    response.raise_for_status()
    data = response.json()["data"]
    if data["fetch"]["httpStatusCode"] >= 400:
        return None
    return {
        "url": data["fetch"]["loadedUrl"],
        "fetched_at": data["fetch"]["loadedTime"],
        "markdown": data["markdown"],
    }
```

Two fields in `data.fetch` matter for an index:

- `loadedUrl` is the address after redirects. Store that one, so a citation points at the page a reader will actually see.
- `httpStatusCode` is what the site itself answered. A site's "Page not found" page still loads, so Grabber returns it with a `200` and charges for it like any loaded page. Check the site's status and skip error pages, or they end up in your index.

If you only need the text, add `unwrap=true` and the response body is the Markdown itself, with `Content-Type: text/markdown`. You lose the fetch details, so for an indexer the envelope is usually worth keeping.

A fetch costs $0.001. Errors, such as a timeout, a blocked address or a site that cannot be reached, cost nothing, and the `X-Grabber-Cost` header on every answer says what that request cost.

### How much smaller is the Markdown?

It depends on the site, so measure your own pages instead of trusting a rule of thumb. Ask for the original body and the Markdown in the same request (several formats cost the same as one) and count tokens with tiktoken:

```python
import tiktoken

response = requests.get(
    API,
    params={"url": "https://docs.python.org/3/tutorial/classes.html", "formats": "raw,markdown"},
    headers=HEADERS,
    timeout=130,
)
data = response.json()["data"]
encoding = tiktoken.get_encoding("o200k_base")
print("HTML tokens:    ", len(encoding.encode(data["raw"])))
print("Markdown tokens:", len(encoding.encode(data["markdown"])))
```

Run it on a handful of the pages you plan to index. Documentation and articles with heavy page templates shrink the most; pages that are mostly text already shrink less.

## Split on headings, not on character counts

The usual first attempt is a fixed-size splitter: a chunk every 1,000 characters, with some overlap. It is simple, and it ignores the document. Markdown gives you something better for free: the author already divided the page into topics, and the headings mark where each one starts.

A good chunker for web Markdown follows four rules:

1. **Start a new chunk at every heading** up to level 3. Deeper headings stay inside their parent.
2. **Keep the heading path** (`Classes > Inheritance > Multiple inheritance`) with the chunk. A paragraph that says "this also applies to subclasses" means little without it.
3. **Split long sections at paragraph breaks**, never mid-sentence, once they pass your size limit. Around 300 to 800 tokens works well with most embedding models.
4. **Never split inside a code block or a table**, and never read a `#` inside a code block as a heading. In a shell snippet, it is a comment.

Here it is in Python. Sizes are in characters, at roughly four characters per token:

```python
import re

HEADING = re.compile(r"^(#{1,3})\s+(.+?)\s*#*$")


def chunk_markdown(markdown, max_chars=3000):
    chunks, path, lines = [], [], []
    in_code = False

    def flush():
        text = "\n".join(lines).strip()
        if text:
            chunks.append({"headings": " > ".join(path), "text": text})
        lines.clear()

    for line in markdown.splitlines():
        if line.lstrip().startswith("```"):
            in_code = not in_code
        heading = None if in_code else HEADING.match(line)
        if heading:
            flush()
            level = len(heading.group(1))
            path[:] = path[: level - 1] + [heading.group(2)]
        lines.append(line)
        if not in_code and not line.strip() and sum(len(l) + 1 for l in lines) > max_chars:
            flush()

    flush()
    return chunks
```

Tables have no blank lines inside them, so a table always stays in one chunk. Embed each chunk with its heading path in front of the text, because a user's question often names the topic ("inheritance") rather than repeating the paragraph.

## Store every chunk with its source

What you store next to each vector decides what your application can do later. Keep at least these:

| Field | Where it comes from | What it is for |
| --- | --- | --- |
| `url` | `data.fetch.loadedUrl` | Citations, and replacing a page's chunks when it changes |
| `headings` | The chunker | Showing where on the page the answer came from |
| `fetched_at` | `data.fetch.loadedTime` | Knowing how old an answer is |
| `hash` | A hash of the page's Markdown | Skipping pages that did not change |

With those, the model can answer with a link to the exact page, and a reader can check it.

## The same pipeline in Node.js

An indexer touches many URLs, so the Node.js version adds two things a real crawl needs: a pause when it hits the rate limit, and a hash to skip pages that did not change. Grabber allows 60 requests a minute per key. Over that it answers `429` with a `Retry-After` header, the number of seconds to wait. Retrying at once does not help, because refused requests count toward the limit too.

```javascript
import { createHash } from 'node:crypto'

const API = 'https://api.grabber.sh/fetch'
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000))

export async function fetchMarkdown(url) {
  for (let attempt = 0; attempt < 3; attempt++) {
    const params = new URLSearchParams({ url, formats: 'markdown' })
    const response = await fetch(`${API}?${params}`, {
      headers: { Authorization: `Bearer ${process.env.GRABBER_API_KEY}` },
      signal: AbortSignal.timeout(130_000),
    })
    if (response.status === 429) {
      await sleep(Number(response.headers.get('retry-after') ?? 60))
      continue
    }
    const body = await response.json()
    if (!response.ok) throw new Error(`${url}: ${body.detail}`)

    const { fetch: info, markdown } = body.data
    if (info.httpStatusCode >= 400) return null
    return {
      url: info.loadedUrl,
      fetchedAt: info.loadedTime,
      hash: createHash('sha256').update(markdown).digest('hex'),
      markdown,
    }
  }
  throw new Error(`${url}: still rate limited after 3 tries`)
}

export async function indexPages(urls, store, embed) {
  for (const url of urls) {
    const page = await fetchMarkdown(url)
    if (!page || (await store.hashOf(page.url)) === page.hash) continue

    const { markdown, ...source } = page
    const chunks = chunkMarkdown(markdown)
    const vectors = await embed(chunks.map((chunk) => `${chunk.headings}\n\n${chunk.text}`))
    const rows = chunks.map((chunk, i) => ({ ...source, ...chunk, vector: vectors[i] }))
    await store.replace(source.url, rows)
  }
}
```

`chunkMarkdown` is the Python function above, line for line. `embed` and `store` stand in for your embedding model and your vector database: `store.replace` deletes a URL's old chunks and writes the new ones, which keeps re-indexing simple.

## Keep the index fresh

Pages change, and a stale index gives confident, outdated answers. A simple routine covers most cases:

- **Re-fetch on a schedule** that matches how often each source changes: daily for news and changelogs, weekly or monthly for documentation.
- **Compare the hash before you embed.** An unchanged page costs one fetch and no embedding calls.
- **Drop URLs that start returning errors**, instead of paying to fetch the same error page every week.

The cost is easy to work out because there is no plan. Re-fetching 10,000 pages once a week is about 43,000 fetches a month, which comes to about $43.

## What about pages that need JavaScript?

Some sites send an empty shell and build the page in the browser, and a plain fetch of those returns little or no Markdown. Add `js_render=true` and Grabber loads the page in a headless browser first, for $0.005 instead of $0.001. Most documentation and articles do not need it, so try without it first. [Static fetch vs JavaScript rendering](https://grabber.sh/blog/static-fetch-vs-javascript-rendering) shows how to tell, and how to fall back automatically.

## FAQ

### Is Markdown better than plain text for embeddings?

For web pages, usually yes. The token counts are close, but Markdown keeps the headings you need for chunk boundaries and heading paths, and keeps tables readable. If your embedding model works better on plain text, strip the Markdown after chunking, not before.

### What chunk size should I use?

Start around 300 to 800 tokens and test with real questions from your users. Smaller chunks match narrow questions better; larger ones carry more context. Keeping the heading path with each chunk matters more than the exact size.

### Can I get PDFs as Markdown?

Yes. Markdown is the default format for HTML pages and PDFs, so the same request works for a link to a PDF.

### Can I get the page's links at the same time?

Yes. `formats=markdown,links` returns the Markdown and the page's links in one request, for the price of one. That is handy for crawling a documentation site one level at a time.

### What does it cost to index 10,000 pages?

$10 for one pass at $0.001 a page, or $50 if every page needed JavaScript rendering. Pages that fail cost nothing, and new accounts get $1 of free credit, enough for the first 1,000 pages.

Source: https://grabber.sh/blog/web-page-to-markdown-for-rag
