---
title: "JavaScript client · Scrape.ly Docs"
description: "Call the Scrape.ly API from Node.js or the browser with scrapely-client."
url: https://scrape.ly/docs/client-js
markdown: https://scrape.ly/docs/client-js.md
---

# JavaScript client

> Call the Scrape.ly API from Node.js or the browser with scrapely-client.

`scrapely-client` is the JavaScript and TypeScript client for the [Scrape.ly API](https://scrape.ly/docs/api-overview.md). Use it from your own backend, script, or service to start Actors, wait for runs, read datasets and key-value stores, and manage tasks, schedules, and webhooks. It is not for writing Actors: code that runs inside an Actor should use the [JavaScript SDK](https://scrape.ly/docs/sdk-node.md).

The source code is on GitHub: [scrapely-app/scrapely-client](https://github.com/scrapely-app/scrapely-client).

The client is a drop-in replacement for `apify-client`. Class and method names match, so most existing `apify-client` code works after you change the import and the base URL.

## Install

```bash
npm install scrapely-client
```

The package ships ES module and CommonJS builds with TypeScript types:

```ts
import { ScrapelyClient } from 'scrapely-client';
// or
const { ScrapelyClient } = require('scrapely-client');
```

`ApifyClient` is exported as an alias of `ScrapelyClient`, so `import { ApifyClient } from 'scrapely-client'` also works.

### Browser and edge runtimes

Bundlers that honour the `browser` export condition pick up a browser bundle automatically. You can also import it explicitly:

```ts
import { ScrapelyClient } from 'scrapely-client/browser';
```

The browser bundle has no proxy support and no log streaming. Edge runtimes may need Node.js compatibility enabled. Do not ship your API token in code that other people load in their browser; call the API from your backend instead.

## Create a client

```ts
import { ScrapelyClient } from 'scrapely-client';

const client = new ScrapelyClient({
    token: process.env.SCRAPELY_TOKEN,
    baseUrl: 'https://api.scrape.ly',
});
```

> [!WARNING]
> Always set `baseUrl` to `https://api.scrape.ly`. The current release defaults to a non-production URL, so a client without `baseUrl` does not talk to your Scrape.ly account.

Create a token in the dashboard under **Settings → API & Integrations**. The client does not read the token or the base URL from environment variables; pass them explicitly. Requests send it as `Authorization: Bearer <token>`.

| Option | Default | Description |
| --- | --- | --- |
| `token` | none | API token. Without it, only public endpoints work. |
| `baseUrl` | non-production URL | API origin, without `/v2`. Set to `https://api.scrape.ly`. |
| `publicBaseUrl` | non-production URL | Origin used when the client builds public storage URLs (for example `createItemsPublicUrl`). Set to `https://api.scrape.ly`. |
| `maxRetries` | `8` | Retries for network errors, `429`, and `5xx` responses. |
| `minDelayBetweenRetriesMillis` | `500` | Initial retry delay. It grows exponentially. |
| `timeoutSecs` | `360` | Upper limit for a single request. |
| `userAgentSuffix` | none | String or array appended to the `User-Agent` header. |
| `requestInterceptors` | `[]` | Axios request interceptors applied to every request. |

In Node.js, outgoing requests honour the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables.

## Run an Actor

`actor(id).call()` starts a run and waits until it finishes. Pass an Actor ID or a `username/actor-name` full name.

```ts
const run = await client.actor('username/my-actor').call({ url: 'https://example.com' });

console.log(run.status); // SUCCEEDED, FAILED, ABORTED, or TIMED-OUT

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

`call()` resolves with the run object for every final status; it does not throw when the run fails. Check `run.status` yourself.

To start a run without waiting, use `start()`, then wait later:

```ts
const run = await client.actor('username/my-actor').start(
    { url: 'https://example.com' },
    { memory: 1024, timeout: 300 },
);

// ...later
const finished = await client.run(run.id).waitForFinish({ waitSecs: 120 });
```

`waitForFinish()` polls until the run reaches a final status or `waitSecs` elapses. Without `waitSecs` it waits indefinitely. It returns the latest run object either way.

On runs of someone else's Actor, `run.buildId`, `run.buildNumber`, and `run.options.build` are missing: build info is owner only, and the run uses the [User Build](https://scrape.ly/docs/publishing.md#user-build).

Run options for `start()` and `call()`:

| Option | Description |
| --- | --- |
| `build` | Build tag or number, for example `latest` or `0.1.4`. Defaults to the Actor's default build. Ignored on someone else's Actor, which always runs the [User Build](https://scrape.ly/docs/publishing.md#user-build). |
| `memory` | Memory in MB. |
| `timeout` | Run timeout in seconds. |
| `maxItems` | Cap on billed dataset items for Actors that charge per result item. |
| `maxTotalChargeUsd` | Cap on the total charge in USD for pay-per-event Actors. |
| `restartOnError` | Restart the run if it fails. |
| `webhooks` | Ad-hoc webhooks for this run only. |
| `contentType` | Content type of the input when you pass a string or Buffer instead of an object. |
| `waitForFinish` | `start()` only. Seconds the API holds the request open (up to 60; the server stops waiting at 45). |
| `waitSecs` | `call()` only. Maximum seconds to wait. Omit to wait indefinitely. |
| `log` | `call()` only, Node.js only. Where to redirect the run log: `'default'` (console, the default), a `Log` instance, or `null` to disable. |

The API returns a run's log as a snapshot rather than a live stream, so the redirected log in `call()` may be incomplete. Fetch the full log after the run with `client.run(run.id).log().get()`.

See [Run Actors](https://scrape.ly/docs/run-actors.md) for how runs, memory, and timeouts work on the platform.

### Other Actor methods

| Method | Description |
| --- | --- |
| `actor(id).get()` | Actor details, or `undefined` if it does not exist. |
| `actor(id).update(fields)` / `.delete()` | Change or remove your Actor. Set the [User Build](https://scrape.ly/docs/publishing.md#user-build) with `update({ defaultNonOwnerBuild: '0.1.4' })`; `null` clears it. |
| `actor(id).build(versionNumber, options)` | Start a build. Options: `tag`, `useCache`, `betaPackages`, `waitForFinish`. |
| `actor(id).defaultBuild()` | Returns a build client for the default build. Owner only; for other Actors read the input schema from `get()` (`defaultBuildActorDefinition`). |
| `actor(id).lastRun({ status })` | Run client for the latest run; supports `.get()`, `.dataset()`, `.keyValueStore()`, `.log()`. |
| `actor(id).runs()` / `.builds()` | Collections of the Actor's runs and builds. Builds are owner only. |
| `actor(id).versions()` / `.version(n)` | Actor versions. Owner only. |
| `actor(id).webhooks()` | Webhooks attached to the Actor. |
| `actors().list({ my, offset, limit, desc })` | Actors you created or used. |
| `actors().create(fields)` | Create an Actor. |

## Runs

```ts
const run = await client.run('RUN_ID').get();
await client.run('RUN_ID').abort({ gracefully: true });
```

| Method | Description |
| --- | --- |
| `run(id).get({ waitForFinish })` | Run details, or `undefined`. |
| `run(id).waitForFinish({ waitSecs })` | Poll until the run finishes. |
| `run(id).abort({ gracefully })` | Abort the run. |
| `run(id).resurrect(options)` | Restart a finished run with the same storages. Options: `build` (your own Actors only), `memory`, `timeout`, `maxItems`, `maxTotalChargeUsd`, `restartOnError`. |
| `run(id).metamorph(targetActorId, input, options)` | Replace the running Actor with another one. |
| `run(id).reboot()` | Restart the run's container. |
| `run(id).update({ statusMessage, isStatusMessageTerminal })` | Update the run's status message. |
| `run(id).charge({ eventName, count, idempotencyKey })` | Charge a pay-per-event event for the run. |
| `run(id).delete()` | Delete the run. |
| `run(id).dataset()` / `.keyValueStore()` / `.requestQueue()` | Clients for the run's default storages. |
| `run(id).log()` | Log client for the run. |
| `runs().list({ status, offset, limit, desc, startedAfter, startedBefore })` | All your runs. `status` accepts a string or an array. |

## Datasets

```ts
const dataset = client.dataset('DATASET_ID');

const page = await dataset.listItems({ limit: 100, fields: ['title', 'url'] });
console.log(page.total, page.items.length);

const csv = await dataset.downloadItems('csv', { bom: true });
```

| Method | Description |
| --- | --- |
| `listItems(options)` | Returns a page `{ items, total, offset, count, limit, desc }`. Options: `offset`, `limit`, `fields`, `omit`, `unwind`, `flatten`, `desc`, `clean`, `skipEmpty`, `skipHidden`, `view`. |
| `downloadItems(format, options)` | Returns a `Buffer`. Formats: `json`, `jsonl`, `csv`, `xlsx`, `xml`, `rss`, `html`. Extra options: `bom`, `delimiter`, `skipHeaderRow`, `xmlRoot`, `xmlRow`, `attachment`. |
| `pushItems(items)` | Append an object or an array of objects. |
| `getStatistics()` | Dataset statistics. |
| `createItemsPublicUrl({ expiresInSecs })` | Signed URL for the items. Defaults to one hour. |
| `get()` / `update(fields)` / `delete()` | Dataset metadata. |
| `datasets().list({ unnamed, offset, limit, desc })` | Your datasets. |
| `datasets().getOrCreate(name)` | Get a named dataset, creating it if needed. |

See [Results](https://scrape.ly/docs/results.md) and [Storage](https://scrape.ly/docs/storage.md) for the data model.

## Key-value stores

```ts
const store = client.keyValueStore('STORE_ID');

await store.setRecord({ key: 'config', value: { depth: 2 } });

const record = await store.getRecord('OUTPUT');
if (record) console.log(record.contentType, record.value);
```

| Method | Description |
| --- | --- |
| `getRecord(key, { buffer, stream })` | Returns `{ key, value, contentType }`, or `undefined` if the key does not exist. JSON is parsed, text is returned as a string, and binary data as a `Buffer`. `stream` is Node.js only. |
| `setRecord({ key, value, contentType })` | Store a record. Without `contentType`, objects are stored as JSON, strings as `text/plain`, and Buffers or streams as `application/octet-stream`. |
| `recordExists(key)` | `true` or `false`. |
| `deleteRecord(key)` | Delete a record. |
| `listKeys({ limit, exclusiveStartKey, prefix, collection })` | One page of keys with `isTruncated` and `nextExclusiveStartKey`. |
| `getRecordPublicUrl(key)` / `createKeysPublicUrl()` | Signed public URLs. |
| `keyValueStores().list()` / `.getOrCreate(name)` | Your stores, or a named store. |

## Request queues

```ts
const queue = client.requestQueue('QUEUE_ID');

await queue.addRequest({ url: 'https://example.com', uniqueKey: 'https://example.com' });
const { items } = await queue.listHead({ limit: 10 });
```

Available methods: `addRequest(request, { forefront })`, `batchAddRequests(requests, { forefront })`, `getRequest(id)`, `updateRequest(request)`, `deleteRequest(id)`, `batchDeleteRequests(requests)`, `listHead({ limit })`, `listAndLockHead({ lockSecs, limit })`, `prolongRequestLock(id, { lockSecs })`, `deleteRequestLock(id)`, `unlockRequests()`, `listRequests()`, `paginateRequests()`, plus `get()`, `update()`, and `delete()`. `requestQueues().getOrCreate(name)` returns a named queue.

`batchAddRequests` splits large arrays into API-sized batches and retries requests the API reports as unprocessed.

## Tasks

Tasks store an Actor's input and run options so you can run the same configuration repeatedly.

```ts
const task = await client.tasks().create({
    actId: 'username/my-actor',
    name: 'daily-crawl',
    input: { url: 'https://example.com' },
});

const run = await client.task(task.id).call();
```

The API requires `actId` and `name`; task names are limited to 30 characters.

| Method | Description |
| --- | --- |
| `task(id).start(input, options)` / `.call(input, options)` | Run the task. The input you pass overrides the stored input. Options match the Actor run options, without `log`. |
| `task(id).getInput()` / `.updateInput(input)` | Read or replace the stored input. |
| `task(id).get()` / `.update(fields)` / `.delete()` | Task details. |
| `task(id).publish({ slug, seoTitle, seoDescription, useCase })` / `.unpublish()` | Publish the task as a public page or take it down. |
| `task(id).lastRun()` / `.runs()` / `.webhooks()` | Related runs and webhooks. |
| `tasks().list()` | Your tasks. |

## Schedules

```ts
await client.schedules().create({
    name: 'nightly',
    cronExpression: '0 3 * * *',
    timezone: 'UTC',
    actions: [{ type: 'RUN_ACTOR', actorId: 'ACTOR_ID' }],
});
```

The API requires `cronExpression` and at least one action. Actions are `RUN_ACTOR` (with `actorId`) or `RUN_ACTOR_TASK` (with `actorTaskId`). `isEnabled` defaults to `true`. `timezone` defaults to `UTC`, and the cron expression is evaluated in it (see [Cron expressions](https://scrape.ly/docs/api-schedules.md#cron-expressions)). Other methods: `schedules().list()`, `schedule(id).get()`, `.update(fields)`, `.delete()`, and `.getLog()`. See [Schedules API](https://scrape.ly/docs/api-schedules.md).

## Automations

```ts
const automation = await client.automations().create({
    title: 'Morning digest',
    trigger: { kind: 'schedule', cron: '0 7 * * 1-5', timezone: 'Europe/Berlin' },
    steps: [{ kind: 'savedInput', savedInputId: 'SAVED_INPUT_ID', inputOverride: { maxItems: 20 } }],
    filters: { onlyNewItems: { keyFields: ['url'] } },
    ai: { mode: 'summarize', prompt: 'The five most interesting new items, with links.' },
    deliveries: [{ channel: 'email' }],
});

const run = await client.automation(automation.id).run();
```

| Method | Description |
| --- | --- |
| `automations().list({ actorId, savedInputId, search, limit, offset, desc })` | Your automations, plain schedules included. `actorId` and `savedInputId` return the automations that use them. |
| `automations().create(automation)` | Create an automation. The body is the one of [Create automation](https://scrape.ly/docs/api-automations.md#create-automation). |
| `automation(id).get()` / `.update(fields)` / `.delete()` | Read, partly update, or delete it. `update` changes only the parts you pass; `ai: null` removes the AI step. |
| `automation(id).run()` | Run it now. Returns the automation run. |
| `automation(id).listRuns({ limit, offset })` / `.getRun(runId)` | Run history and one run, with each step's run, the filter result, `aiOutput`, `aiCostUsd`, and delivery statuses. |
| `automation(id).duplicate({ title, name, isEnabled })` | Copy it. The copy is paused unless `isEnabled` is `true`. |

`automations()` is Scrape.ly-only and comes with version 2.25.9 of `scrapely-client`.

## Webhooks

```ts
await client.webhooks().create({
    eventTypes: ['ACTOR.RUN.SUCCEEDED', 'ACTOR.RUN.FAILED'],
    condition: { actorId: 'ACTOR_ID' },
    requestUrl: 'https://example.com/hooks/scrapely',
});
```

The API requires `requestUrl` and at least one event type. Other methods: `webhooks().list()`, `webhook(id).get()`, `.update(fields)`, `.delete()`, `.test()`, `.dispatches()`, and `webhookDispatches().list()`. See [Webhooks API](https://scrape.ly/docs/api-webhooks.md) for event types and payloads.

## Builds

| Method | Description |
| --- | --- |
| `build(id).get({ waitForFinish })` | Build details. |
| `build(id).waitForFinish({ waitSecs })` | Poll until the build finishes. |
| `build(id).abort()` / `.delete()` | Abort or delete a build. |
| `build(id).log()` | Build log client. |
| `builds().list()` | All your builds. |

## Account

```ts
const me = await client.user().get();
const usage = await client.user().monthlyUsage();
const limits = await client.user().limits();
```

`user()` defaults to the account that owns the token.

## Store

```ts
const { items } = await client.store().list({ search: 'google maps', limit: 10 });
```

Options: `search`, `limit` (the API defaults to 20 and caps it at 100), `offset`, `sortBy`, `category`, `username`, and `pricingModel`. See [Store API](https://scrape.ly/docs/api-store.md).

## Logs

```ts
const text = await client.run('RUN_ID').log().get();
const buildLog = await client.build('BUILD_ID').log().get();
```

`log(id)` on the root client accepts a run ID or a build ID. `get()` returns the log as a string, or `undefined` if it does not exist. In Node.js, `stream()` returns the log as a `Readable`. The API sends the log as it is at request time and then closes the connection, so to follow a running Actor, poll `get()`.

## Pagination

List methods return a value that is both a promise of one page and an async iterable over every item:

```ts
// One page
const page = await client.actors().list({ limit: 20 });

// Every item, fetched page by page
for await (const run of client.runs().list({ desc: true })) {
    console.log(run.id, run.status);
}

for await (const item of client.dataset('DATASET_ID').listItems({ chunkSize: 1000 })) {
    // ...
}
```

`chunkSize` sets how many items each underlying request fetches while iterating. `limit` and `offset` still bound the overall range. `keyValueStore(id).listKeys()` iterates the same way using `exclusiveStartKey`.

## Errors and retries

The client retries network errors, invalid response bodies, `429`, and `5xx` responses up to `maxRetries` times with exponential backoff. Other `4xx` responses fail immediately. Each attempt's timeout doubles, up to `timeoutSecs`. Streaming uploads are not retried.

Failed API calls throw `ScrapelyApiError` (also exported as `ApifyApiError`):

```ts
import { ScrapelyApiError } from 'scrapely-client';

try {
    await client.actor('username/missing').start();
} catch (err) {
    if (err instanceof ScrapelyApiError) {
        console.error(err.statusCode, err.type, err.message, err.attempt);
    }
    throw err;
}
```

The error carries `statusCode`, `type`, `message`, `attempt`, `httpMethod`, `path`, `clientMethod`, and `data`. See [API overview](https://scrape.ly/docs/api-overview.md) for error types.

`get()` methods do not throw on `404`; they return `undefined`. Date fields whose names end in `At` are converted to `Date` objects.

## Migrating from apify-client

- Replace the import with `scrapely-client`. `ApifyClient` and `ApifyApiError` still work as aliases.
- Set `baseUrl` (and `publicBaseUrl` if you build public URLs) to `https://api.scrape.ly`.
- Use a Scrape.ly token. Apify tokens do not work.
- `store().list()` calls `GET /v2/store/actors`.
- `task(id).publish()` and `.unpublish()` are Scrape.ly additions.
- Endpoints that Scrape.ly does not implement return an error. The [API reference](https://scrape.ly/docs/api.md) lists what is available.

---

- Previous: [Tasks, schedules & webhooks](https://scrape.ly/docs/integrations.md)
- Next: [Python client](https://scrape.ly/docs/client-python.md)
- All docs: [Docs index](https://scrape.ly/docs.md), [llms.txt](https://scrape.ly/llms.txt)
