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

# Node.js

> Install the official scrapeunblocker npm package, or call the plain HTTP API directly.

The official Node.js / TypeScript client wraps the ScrapeUnblocker API in a small, fully typed package - typed errors, automatic retries, ESM + CommonJS, zero runtime dependencies.

```bash theme={null}
npm install scrapeunblocker
```

Requires Node.js 18+.

<Note>
  Prefer no dependencies? The API is plain HTTP, so you can skip the package and
  use Node's built-in `fetch` directly - see [Raw HTTP](#raw-http-without-the-package) at the bottom.
</Note>

Store your API key in an environment variable so the client picks it up automatically:

```bash theme={null}
export SCRAPEUNBLOCKER_KEY="YOUR_API_KEY"
```

## Quickstart

```ts theme={null}
import { ScrapeUnblockerClient } from "scrapeunblocker";

const su = new ScrapeUnblockerClient(); // reads SCRAPEUNBLOCKER_KEY, or { apiKey: "YOUR_API_KEY" }

const html = await su.getPageSource("https://example.com");
```

CommonJS works too: `const { ScrapeUnblockerClient } = require("scrapeunblocker");`

## Get parsed JSON instead of HTML

`getParsed()` returns structured data extracted using Schema.org, `__NEXT_DATA__`, or AI-generated rules - no per-site parsers to maintain.

```ts theme={null}
const product = await su.getParsed("https://www.amazon.com/dp/B08N5WRWNW");

console.log(product.pageType); // "product"
console.log(product.source);   // how it was extracted
console.log(product.data);     // the fields
```

If a parse ever comes back wrong, force a fresh set of rules:

```ts theme={null}
const fresh = await su.getParsed(url, { refreshRules: true, rulesHint: "price is missing" });
```

See the [parsed data guide](/guides/parsed-data) for response shapes.

## Scrape a Google SERP

```ts theme={null}
const serp = await su.serp("best running shoes", { pagesToCheck: 2 });

for (const result of serp.organic) {
  console.log(result.position, result.title, result.url);
}
```

## Force a country

`proxyCountry` works on every method.

```ts theme={null}
const result = await su.getParsed("https://www.amazon.de/dp/B08N5WRWNW", { proxyCountry: "de" });
```

## Capture cookies and the proxy used

```ts theme={null}
const page = await su.getPageWithCookies("https://example.com");
console.log(page.html, page.cookies, page.proxy);
```

## Fetch an image

```ts theme={null}
import { writeFile } from "node:fs/promises";
const bytes = await su.getImage("https://example.com/photo.jpg");
await writeFile("photo.jpg", bytes);
```

## Drive the browser with steps

Pass a native array of actions to run in a real browser after the page loads. You
get back the HTML of the resulting state.

```ts theme={null}
const html = await su.getPageSource("https://example.com", {
  steps: [
    { action: "type", selector: "input[name=q]", value: "wireless headphones" },
    { action: "press_key", value: "Enter" },
    { action: "wait_for", selector: ".search-results", timeout_ms: 10000 },
  ],
});
```

Discover the selectors first with `listElements()`, which returns the page's
interactive elements as parsed JSON:

```ts theme={null}
const page = await su.listElements("https://example.com");

for (const el of page.elements) {
  console.log(el.tag, el.selector, el.text);
}
```

Each element's `selector` drops straight into a `steps` action. Full action
reference in the [browser steps guide](/guides/browser-steps).

## Amazon

Product and search data as JSON, priced in the marketplace's own currency:

```ts theme={null}
// One product by ASIN (or { url: "https://www.amazon.de/dp/B0BSHF7WHW" })
const product = await su.amazonProduct({ asin: "B0BSHF7WHW", marketplace: "amazon.com" });
console.log(product.title, product.price, product.currency, product.rating);

// Keyword search
const results = await su.amazonSearch("wireless headphones", { sort: "price_asc" });
for (const item of results.results) {
  console.log(item.title, item.price, item.currency, item.asin);
}
```

`proxyCountry` defaults to the marketplace's home country (`amazon.com` -> US, `amazon.de` -> DE), so prices come back in the right currency with no configuration. Full field list on the [Amazon plugin page](/plugins/amazon).

## eBay search

Listings from any of the 19 regional eBay marketplaces as JSON:

```ts theme={null}
const items = await su.ebaySearch("iphone 13", {
  marketplace: "ebay.com",
  condition: "used",
  sort: "newly_listed",
});

if (items.exactMatches) {
  for (const item of items.results) {
    console.log(item.title, item.price, item.currency, item.condition);
  }
}
```

`exactMatches` is `false` when eBay found nothing for the keyword and answered with its own loosely-related suggestions instead. Full field list on the [eBay Search plugin page](/plugins/ebay).

## Skyscanner plugins

Flights, hotels and car hire as JSON:

```ts theme={null}
const locations = await su.skyscanner.flightLocations("London");

const flights = await su.skyscanner.flights({
  origin: "London", dest: "New York",
  depart_date: "2026-09-01", adults: 1, currency: "USD",
});

const hotels = await su.skyscanner.hotels({ destination: "Madrid", checkin: "2026-09-01", checkout: "2026-09-03" });
const cars = await su.skyscanner.carhire({ pickup: "Madrid", pickup_datetime: "2026-09-01T10:00", dropoff_datetime: "2026-09-03T10:00" });
```

## Concurrency

Every method returns a promise, so cap concurrency with a semaphore for high-throughput crawls:

```ts theme={null}
import { ScrapeUnblockerClient } from "scrapeunblocker";

const su = new ScrapeUnblockerClient();
const CONCURRENCY = 10;

async function scrapeAll(urls: string[]) {
  const results: unknown[] = [];
  const queue = [...urls];
  async function worker() {
    let url;
    while ((url = queue.shift())) results.push(await su.getParsed(url));
  }
  await Promise.all(Array.from({ length: CONCURRENCY }, worker));
  return results;
}
```

## Error handling

Non-2xx responses reject with typed errors, all subclasses of `ScrapeUnblockerError`. Transient failures (429, 502, 503, 504 and network errors) are retried automatically with exponential backoff; a 401 or 402 is never retried, because it clears when the key or the billing state changes.

```ts theme={null}
import {
  ScrapeUnblockerClient,
  BlockedError,
  PaymentRequiredError,
  RateLimitError,
  UpstreamOutageError,
} from "scrapeunblocker";

const su = new ScrapeUnblockerClient();
try {
  await su.getPageSource("https://example.com");
} catch (err) {
  if (err instanceof BlockedError) {
    // 403: the target blocked every bypass path (not billed)
  } else if (err instanceof PaymentRequiredError) {
    // 402: quota, credit limit, or a failed payment - fix billing
  } else if (err instanceof RateLimitError) {
    // 429: slow down
  } else if (err instanceof UpstreamOutageError) {
    // 503: the target site itself is down - retry later
  }
}
```

| Error                      | Status | Meaning                                                           |
| -------------------------- | ------ | ----------------------------------------------------------------- |
| `InvalidRequestError`      | 400    | Bad URL, unsupported scheme, or the API key header was not sent   |
| `AuthenticationError`      | 401    | Key not recognised - typo, stray whitespace, or a rotated key     |
| `NoSubscriptionError`      | 401    | Key is fine, but the account has no active plan                   |
| `PaymentRequiredError`     | 402    | Billing block - base class for the three below                    |
| `QuotaExceededError`       | 402    | The plan's requests for this period are used up                   |
| `CreditLimitExceededError` | 402    | Unpaid balance is past the account's credit limit                 |
| `PaymentFailedError`       | 402    | A card payment was declined three times                           |
| `BlockedError`             | 403    | Blocked by bot protection on every path                           |
| `NotFoundError`            | 404    | Page loaded but held no image (`getImage` only)                   |
| `BrowserTimeoutError`      | 408    | Our browser run timed out before the page was ready               |
| `UnsupportedContentError`  | 415    | The URL serves something other than HTML                          |
| `ValidationError`          | 422    | Missing or wrong-typed parameter; `body` holds the `detail` array |
| `RateLimitError`           | 429    | Too many requests                                                 |
| `UpstreamOutageError`      | 503    | The target origin is down                                         |
| `ServerError`              | 5xx    | Unexpected server error, including a 504 upstream timeout         |
| `ScrapeTimeoutError`       | -      | This client gave up locally before the API answered               |
| `ConnectionError`          | -      | Could not reach the API                                           |

<Note>
  The `402` and `404`-`422` errors were added in **0.1.6**; before that they arrived as a bare `APIError`. Nothing was removed, so `instanceof APIError` checks written against earlier versions keep working. See all status codes in [Errors](/errors).
</Note>

Tune the retry count and timeout on the client:

```ts theme={null}
new ScrapeUnblockerClient({ timeout: 180000, maxRetries: 2 });
```

See [handling failures](/guides/handling-failures) for what each status code means.

## Raw HTTP (without the package)

The API is plain HTTP, so Node's built-in `fetch` (Node 18+) is enough - no SDK required.

```ts theme={null}
const response = await fetch(
  "https://api.scrapeunblocker.com/getPageSource?url=https://example.com&parsed_data=true",
  { method: "POST", headers: { "x-scrapeunblocker-key": process.env.SCRAPEUNBLOCKER_KEY } },
);
const payload = await response.json();
console.log(payload.data.page_type);
```

The endpoints are `getPageSource`, `serpApi`, and `getImage`; every parameter shown above maps to a query parameter of the same name.
