> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The Zenrows API (Fetch, Extract, Crawl and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Every endpoint also has its own page under /api-reference with its schema. Use the spec for exact parameter names and types. To call Zenrows from an agent, connect the MCP server at https://mcp.zenrows.com/mcp. To search these docs from an agent, connect the docs MCP server at https://docs.zenrows.com/mcp. Agent skills for Zenrows are published at https://github.com/ZenRows/cli and install with `npx skills add ZenRows/cli`.

# How to Integrate the Vercel AI SDK with Zenrows

> Install the @zenrows/ai-sdk-tools package to give Vercel AI SDK agents live web access: scrape any page, extract named fields, and screenshot layouts.

Give a Vercel AI SDK agent live access to the protected web. The [`@zenrows/ai-sdk-tools`](https://www.npmjs.com/package/@zenrows/ai-sdk-tools) package exposes Zenrows as three ready-made AI SDK tools, so a model can read a page, pull named fields out of it, or look at how it renders, without you writing a tool definition or a fetch wrapper. This guide covers installing the package, wiring it into `generateText` or `streamText`, and controlling what each call costs.

## What is the Vercel AI SDK?

The [Vercel AI SDK](https://ai-sdk.dev) is a TypeScript toolkit for building applications with large language models. It gives you one interface across model providers, and it runs the tool-calling loop for you: you hand it a set of tools, and it decides when to call them, feeds the results back to the model, and keeps going until the model has an answer.

A tool in the AI SDK is a description the model can read, a schema for its arguments, and a function that runs when the model calls it. `@zenrows/ai-sdk-tools` ships three of them already written.

### Key benefits of integrating the Vercel AI SDK with Zenrows

* **No tool definitions to write**: one factory call returns three tools with descriptions and schemas the model already understands.
* **Access to the protected web**: pages behind antibot systems, JavaScript rendering, or geographic restrictions come back as ordinary text.
* **Output a model can read**: pages arrive as Markdown rather than raw HTML, so you spend context on content rather than markup.
* **Fewer tokens when you know what you want**: `extractData` returns named fields instead of a whole page.
* **Vision when layout is the question**: `takeScreenshot` hands a real image part to the model, not a base64 string.
* **Spend visible per call**: every result carries what the request cost, in credits and in US dollars.

## Use cases

* **Research agents**: let a model read several live sources and answer from them, rather than from training data.
* **Competitive monitoring**: pull prices, titles, or availability from pages that block ordinary requests.
* **Retrieval over the live web**: fetch and ground answers in pages that are too fresh to be indexed.
* **Layout and rendering questions**: ask a vision model what a page looks like, not just what it says.
* **Lead and catalog enrichment**: turn a URL into named fields a downstream step can consume.

## Prerequisites

* **Node.js 22 or later.** The package declares `"node": ">=22"`.
* **A Zenrows API key**, from the [API keys page](https://app.zenrows.com/settings/api-keys) in your dashboard.
* **The AI SDK and Zod**, which are peer dependencies: `ai` `^7.0.0` and `zod` `^3.25.76 || ^4.1.8`.
* **A model provider**, for example `@ai-sdk/anthropic` or `@ai-sdk/openai`, with its own API key.

## Getting started: basic usage

Install the package:

```bash npm theme={"theme":"css-variables"}
npm install @zenrows/ai-sdk-tools
```

```bash pnpm theme={"theme":"css-variables"}
pnpm add @zenrows/ai-sdk-tools
```

```bash yarn theme={"theme":"css-variables"}
yarn add @zenrows/ai-sdk-tools
```

```bash bun theme={"theme":"css-variables"}
bun add @zenrows/ai-sdk-tools
```

Call `createZenrowsTools` with your API key and pass the result straight to `generateText` as its `tools`:

```ts TypeScript theme={"theme":"css-variables"}
import { generateText, isStepCount } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { createZenrowsTools } from "@zenrows/ai-sdk-tools";

const zenrows = createZenrowsTools({
  apiKey: process.env.ZENROWS_API_KEY!,
});

const { text } = await generateText({
  model: anthropic("claude-sonnet-5-5"),
  prompt:
    "Read https://www.scrapingcourse.com/ecommerce/ and name three products with their prices.",
  tools: zenrows,
  stopWhen: isStepCount(3),
});

console.log(text);
```

The model calls `scrapeUrl` on its own, reads the page, and answers:

```text Output theme={"theme":"css-variables"}
1. **Abominable Hoodie** - $69.00
2. **Adrienne Trek Jacket** - $57.00
3. **Aeon Capri** - $48.00
```

> [!NOTE]
>
> `stopWhen` is what lets the model take more than one turn. Without it the SDK stops after the first step, so the model calls a tool and never gets to use the result. `isStepCount(3)` gives it room to call a tool, read the output, and answer.

The same `zenrows` object works unchanged with `streamText` and with the SDK's `Agent` class.

## The three tools

`createZenrowsTools` returns an object with exactly these keys, so you can pass all three or hand the model a subset.

### scrapeUrl

Returns a whole page as Markdown, plain text, or HTML. Use it when the model needs to read something and you do not know in advance which parts matter.

Returns `{ content, truncated, url, finalUrl, creditsSpent, costUsd }`.

```ts TypeScript theme={"theme":"css-variables"}
const result = await zenrows.scrapeUrl.execute(
  { url: "https://www.scrapingcourse.com/ecommerce/" },
  { toolCallId: "example", messages: [] },
);

console.log(result.truncated, result.creditsSpent, result.costUsd);
```

```text Output theme={"theme":"css-variables"}
false 1 0.001
```

### extractData

Returns named fields as JSON instead of a whole page. Far cheaper in context than reading the page and asking the model to find the fields itself.

Returns `{ data, url, creditsSpent, costUsd }`, plus `note` when Zenrows has something to say about the result.

```ts TypeScript theme={"theme":"css-variables"}
const result = await zenrows.extractData.execute(
  {
    url: "https://www.scrapingcourse.com/ecommerce/",
    cssSelectors: { title: ".product-name", price: ".price" },
  },
  { toolCallId: "example", messages: [] },
);

console.log(JSON.stringify(result.data));
```

```json Output theme={"theme":"css-variables"}
{
  "price": ["$69.00", "$57.00", "$48.00", "$24.00"],
  "title": ["Abominable Hoodie", "Adrienne Trek Jacket", "Aeon Capri", "Aero Daily Fitness Tee"]
}
```

> [!WARNING]
>
> `cssSelectors` returns **one array per field, in page order**, not an array of paired objects. Pairing the first title with the first price is the model's job, or yours. Fields that match a different number of elements will not line up.

Pass `outputs` instead of `cssSelectors` to collect one kind of thing across the whole page. Accepted values are `emails`, `phone_numbers`, `headings`, `images`, `audios`, `videos`, `links`, `menus`, `hashtags`, `metadata`, `tables`, and `favicon`.

```ts TypeScript theme={"theme":"css-variables"}
const links = await zenrows.extractData.execute(
  { url: "https://www.scrapingcourse.com/ecommerce/", outputs: ["links"] },
  { toolCallId: "example", messages: [] },
);
```

With neither option, Zenrows decides what the page is about and extracts accordingly.

### takeScreenshot

Captures the rendered page and hands the model a real image part through the AI SDK's `toModelOutput`, so a vision-capable model sees the page rather than a base64 string. Your own code gets the image as base64 in `{ image, mediaType, bytes }`.

```ts TypeScript theme={"theme":"css-variables"}
const shot = await zenrows.takeScreenshot.execute(
  { url: "https://www.scrapingcourse.com/ecommerce/" },
  { toolCallId: "example", messages: [] },
);

console.log(shot.mediaType, shot.bytes, shot.creditsSpent);
```

```text Output theme={"theme":"css-variables"}
image/png 245334 5
```

Use it for questions about layout, placement, or visual state. For questions about content, `scrapeUrl` is cheaper in both credits and tokens.

## Configuration options

These are set once on the factory and are never exposed to the model, so it cannot change them mid-run.

| Option | Type | Default | Description |
| - | - | - | - |
| `apiKey` | `string` | **Required.** | Your Zenrows API key. |
| `adaptiveStealth` | `boolean` | `true` | Let Zenrows decide per request what the page needs, and bill for the configuration that worked. |
| `defaultResponseType` | `string` | `markdown` | What `scrapeUrl` returns: `markdown`, `plaintext`, or `html`. |
| `maxContentLength` | `number` | `100000` | Characters above which `scrapeUrl` truncates and sets `truncated: true`. |
| `concurrency` | `number` | `5` | Requests in flight at once. |
| `retries` | `number` | `0` | Retries per failed request. |

### On adaptive stealth

Zenrows can decide per request whether a page needs JavaScript rendering or a premium proxy, and bills for the configuration that worked. That is on by default, which is why the model is given no `jsRender` or `premiumProxy` knob to guess at.

Zenrows rejects adaptive stealth combined with an explicit tier flag, so an option that needs a browser, such as a screenshot or a country-specific proxy, drops it for that one call and sets what the option actually needs instead. The model cannot produce a request Zenrows will refuse.

Set `adaptiveStealth: false` if you would rather pay a flat, predictable cost per request and accept that protected pages will fail.

### On content length

A scraped page goes into the model's context and you pay for every character of it. HTML routinely runs past 80,000 characters for an ordinary page, so `scrapeUrl` truncates at `maxContentLength` and sets `truncated: true` rather than quietly sending a large bill. Prefer `extractData` when you already know which fields you want.

## Cost and credits

Zenrows bills per request. Every tool result carries `creditsSpent`, the credits the request used, read from the `X-Request-Credits` response header, and `costUsd`, its cost in US dollars, from `X-Request-Cost`. A model can therefore report what a piece of research cost.

| Call | Credits |
| - | - |
| `scrapeUrl` on an ordinary page | 1 |
| `extractData` on an ordinary page | 1 |
| `takeScreenshot` | 5 |

Protected pages cost more, because adaptive stealth bills for the configuration that succeeded. See [Pricing and credits](/first-steps/pricing) for the full model.

Two cases are not charged at all:

* **Extract recognises the page but finds no data on it.** Zenrows answers `422 no_data_extracted`. `extractData` returns that as an `error` telling the model to read the page with `scrapeUrl` instead of repeating the call.
* **Extract refuses the page.** Zenrows answers `403 REQS007`, also not charged. It covers three different situations: the domain is not prepared, the domain is prepared but Extract did not recognise this particular page, or Extract is not enabled for the site. See [REQS007](/api-error-codes#REQS007).

> [!NOTE]
>
> The tools return `{ error }` rather than throwing. A failed call costs the agent a step, not the whole run, and the message is written for a model to act on.

## Troubleshooting

### The model calls a tool and then stops

Set `stopWhen`. Without it the AI SDK stops after the first step, so the model makes the tool call and never sees the result. `isStepCount(3)` is enough for read, think, answer.

### The run uses far more tokens than expected

A whole page is going into context. Switch the call to `extractData` with `cssSelectors`, or lower `maxContentLength` so `scrapeUrl` truncates sooner. Check `truncated` on the result to see whether it is already happening.

### `extractData` returns an error saying no data was extracted

Zenrows recognised the page but none of the requested fields came back filled. Check the selectors against the live page, or read it with `scrapeUrl` instead. The request was not charged.

### `extractData` returns the whole page instead of named fields

The option is `cssSelectors`, not `selectors`. An unrecognised key is dropped by the schema and the call runs with no options, which makes Zenrows decide what to extract.

### A tool reports an invalid API key

Confirm `ZENROWS_API_KEY` is set in the environment the code actually runs in, and that the key is enabled on the [API keys page](https://app.zenrows.com/settings/api-keys). Note that `403 REQS007` is about Extract refusing the page, not about the key.

### `extractData` returns a 403 REQS007

The message you get back says Extract is not prepared for the URL. Read that as "Extract refused this page", because `REQS007` covers three situations and the message does not yet tell you which one you hit.

**Do this first, because it works in all three cases:** call `extractData` again with `cssSelectors` or `outputs`, which need no preparation, or read the page with `scrapeUrl`. The request was not charged either way.

**Preparing the domain only helps one of the three.** If the domain is genuinely unprepared, [prepare it](/extract/endpoints#getting-a-domain-prepared) and retry. If Extract simply did not recognise this page, which is common on home and index pages, the domain is already prepared and preparing it again returns the same `ready` record and the same error. If Extract is not enabled for the site, preparing does nothing either.

[REQS007](/api-error-codes#REQS007) tells the three apart by the `title` the API returns. Note that the AI SDK tools do not surface `title`, so to distinguish the cases you need to call the API directly.

### Screenshots are expensive

A screenshot costs five credits against one for a scrape, because it needs a browser. Use it only for questions about layout. For content, `scrapeUrl` answers the same question for a fifth of the price.

## Helpful resources

* [`@zenrows/ai-sdk-tools` on npm](https://www.npmjs.com/package/@zenrows/ai-sdk-tools)
* [Source and issues on GitHub](https://github.com/ZenRows/vercel-zenrows)
* [AI SDK tool calling documentation](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling)
* [Zenrows API reference](/fetch/api-reference#parameter-overview)
* [Zenrows MCP server](/mcp/overview), if you would rather connect Zenrows over MCP than install a package

## Frequently asked questions

> **Which model providers does this work with?**
>
> All of them. The tools are plain AI SDK tools, so they work with any provider the SDK supports. Tool calling has to be supported by the model you choose.

> **Can I give the model only some of the tools?**
>
> Yes. `createZenrowsTools` returns an object keyed by `scrapeUrl`, `extractData`, and `takeScreenshot`. Pass only the keys you want as `tools`.

> **Does it work with streamText and the Agent class?**
>
> Yes. The same object passed as `tools` works unchanged in `generateText`, `streamText`, and the SDK's `Agent`.

> **Do I need to enable JavaScript rendering myself?**
>
> No. Adaptive stealth is on by default and decides per request, which is why the model is given no rendering options to guess at. Turn it off with `adaptiveStealth: false` if you want a flat cost per request.

> **How do I know what a run cost?**
>
> Every tool result carries `creditsSpent` and `costUsd`. Sum them across the steps of a run, or let the model report them.

> **Why does extractData return arrays instead of objects?**
>
> Each CSS selector is matched across the whole page, so each field comes back as an array in page order. Pairing them is left to you or the model, because fields do not always match the same number of elements.

> **Can I use the Zenrows MCP server with the AI SDK instead?**
>
> The AI SDK can connect to MCP servers, but this package is the supported route for the AI SDK and needs no server connection. See the [MCP overview](/mcp/overview) for what the MCP server offers.

> **Is CommonJS supported?**
>
> Yes. The package ships both ESM and CommonJS builds with TypeScript types.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.