---
title: "Actors · Scrape.ly Docs"
description: "Create, run, version, build, and publish Actors, plus CI and Store endpoints."
url: https://scrape.ly/docs/api-actors
markdown: https://scrape.ly/docs/api-actors.md
---

# Actors

> Create, run, version, build, and publish Actors, plus CI and Store endpoints.

Create and manage Actors, versions, builds, and runs. All paths are relative to `https://api.scrape.ly`.

Actor IDs may be the opaque ID or the named ID `username~actor-name`. Public Actors can be read without a token. Private Actors need the owner's token. Builds and versions are owner only, also for public Actors.

Most Actor paths use `/v2/actors`. A few also answer under `/v2/acts`: `GET /v2/acts/{actorId}`, `GET /v2/acts/{actorId}/picture`, and `GET /v2/acts/{actorId}/webhooks`. The example-task endpoints under `/v2/acts/{actorId}/examples` are covered in [Actor tasks](https://scrape.ly/docs/api-tasks.md#public-example-tasks).

## List Actors

```
GET /v2/actors
```

Requires authentication. With `my=1`, returns Actors you own. Otherwise returns Actors you created **or** have run.

| Query param | Type | Description |
| --- | --- | --- |
| `my` | boolean | `1` / `true`: only Actors you own. Default is created-or-used. |
| `sortBy` | string | `createdAt` (default), `stats.lastRunStartedAt`, `name`, or `totalRuns`. Other values fall back to `createdAt`. |
| `search` | string | Filter by Actor name. |
| `status` | string | Filter by the status of **your** last run of that Actor (for example `SUCCEEDED`). |
| `pricingModel` | string | `PAY_PER_EVENT` or `FREE`. Any other model returns `400 invalid-pricing`. |
| `hasSuccessfulBuild` | boolean | When listing created-or-used (`my` unset), only Actors with a successful build. Applies to your own Actors only; Actors you've used are always included. |
| `limit` / `offset` / `desc` | number / boolean | [Pagination](https://scrape.ly/docs/api-overview.md#pagination). Default `limit` is `1000`. |

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actors?my=1&sortBy=totalRuns&desc=1&limit=20"
```

```api-test
{ "method": "GET", "path": "/v2/actors?my=1&limit=5" }
```

## Create Actor

```
POST /v2/actors
```

Accepts a user API key, an OAuth token, or a full-permission scoped token. Limited scoped tokens get `403 insufficient-permissions`. Counts against your plan's Actor limit. Returns `201` and a `Location` header.

| Body field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Unique Actor name (max 40 characters). |
| `title` | string | No | Display title. |
| `description` | string | No | Short description (max 500 characters). |
| `seoTitle` / `seoDescription` | string | No | SEO metadata for the public page. |
| `isPublic` | boolean | No | Whether the Actor is listed in the Store. |
| `versions` | array | No | Initial versions (see [Versions](#versions)). |
| `defaultRunOptions` | object | No | Default `build`, `memoryMbytes`, `timeoutSecs`, `maxItems`, `restartOnError`, `forcePermissionLevel`, `minimalMaxTotalChargeUsd`. |
| `exampleRunInput` | object | No | Example input (`body`, `contentType`). |
| `pricingInfos` | array | No | Pricing configuration. |
| `actorStandby` | object | No | [Standby](https://scrape.ly/docs/api-standby.md) settings. |
| `categories` | string[] | No | Store categories. |
| `iconWebp` | string | No | Icon as base64 WebP or a `data:image/...;base64,` URL (PNG and JPEG are converted to WebP). Responses never include the icon itself; they return `pictureUrl`. Sending the current `pictureUrl` back leaves the icon unchanged. |
| `hideSourceFiles` | boolean | No | Hide source files, the Dockerfile and build logs from non-owners. Defaults to `true`. Hiding is always allowed, also while the Actor is public. Setting `false` on a hidden Actor needs `confirmPublicSourceFiles`. |
| `confirmPublicSourceFiles` | string | With `hideSourceFiles: false` | The Actor name, to confirm that you want to make the source public. Not stored. |

| Status | Type | Meaning |
| --- | --- | --- |
| 400 | `invalid-input` | Name over 40 characters, description over 500, or `defaultRunOptions.timeoutSecs` out of range. |
| 400 | `profanity-detected` | The name, title, or description contains blocked language. |
| 400 | `source-visibility-confirmation-required` | `hideSourceFiles: false` was sent without `confirmPublicSourceFiles` set to the Actor name. |
| 403 | `limit-reached` | You already have as many Actors as your plan allows. |

```bash
curl -X POST https://api.scrape.ly/v2/actors \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-actor",
    "title": "My Actor",
    "isPublic": false,
    "versions": [{
      "versionNumber": "0.1",
      "buildTag": "latest",
      "sourceType": "SOURCE_FILES",
      "sourceFiles": [
        { "name": "main.js", "format": "TEXT", "content": "import { Actor } from \"scrapely\";" }
      ]
    }]
  }'
```

## Get Actor

```
GET /v2/actors/{actorId}
GET /v2/acts/{actorId}
```

Public Actors are readable without a token. Private Actors need the owner. Authenticated callers get `userStats` for their own use of the Actor. Scoped run tokens with limited permissions get the non-owner view, also for the runner's own Actors.

Usage numbers are owner only too: for non-owners and anonymous callers, the only count in `stats` is `totalUsers30Days` (monthly active users: every distinct user with a run in the last 30 days); `totalRuns`, `totalUsers`, `totalUsers7Days`, `totalUsers90Days`, `totalMetamorphs` and `totalBookmarks` are `null`, and your own run count is in `userStats`. Every caller, the owner included, also gets `stats.successRate`: the percentage of the Actor's finished runs (succeeded, failed or timed out; aborted and unfinished runs are left out) that succeeded, and `stats.successRateWindow`, which says which runs it covers. Every run counts, whoever started it. The window is the runs started since the Actor's last successful build (`"currentBuild"`); editing the description, settings or price does not reset it, a new build does. If the current build has fewer than 10 finished runs, the rate covers the last 30 days across builds (`"last30Days"`). If that also has fewer than 10, both fields are `null` ("Not enough runs yet"). No run counts are published with it.

Build info is owner only, also for public Actors. Non-owners don't get `versions`, `taggedBuilds`, `defaultNonOwnerBuild`, `defaultRunOptions.build`, `stats.totalBuilds`, `hasSuccessfulBuild`, or standby config. Their `defaultBuildActorDefinition` comes from the [User Build](https://scrape.ly/docs/publishing.md#user-build) and holds only what's needed to run the Actor: `actorSpecification`, `name`, `title`, `description`, `readme`, `input`/`inputSchema`, `output`, `storages`, the memory limits, `usesStandbyMode`, `webServerSchema`, and `webServerMcpPath`. Build details such as `version`, `buildTag`, `changelog`, `dockerfile`, and `environmentVariables` are left out. Source files are left out when `hideSourceFiles` is set; when they're shown, `CHANGELOG.md` is still left out.

The input schema lives on the build's Actor definition (`defaultBuildActorDefinition.input` or `.inputSchema`), not on a separate schema endpoint. Owners get the default build's definition; everyone else gets the User Build's.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/actors/janedoe~my-actor
```

```api-test
{ "method": "GET", "path": "/v2/actors/janedoe~my-actor" }
```

## Get Actor icon and picture

```
GET /v2/actors/{actorId}/picture.webp?v={hash}
GET /v2/actors/{actorId}/icon
GET /v2/actors/{actorId}/picture
GET /v2/acts/{actorId}/picture
```

No authentication. API objects never embed the icon. Actors, Store items, bookmarks and search results carry `pictureUrl`, and runs and tasks carry `actorPictureUrl` (`null` when the Actor has no icon). Use the URL directly in an `<img>` tag.

- `pictureUrl` is `/picture.webp?v={hash}`, where the hash identifies the icon's content. It works for any Actor whose icon has that hash, and returns the raw image with `Cache-Control: public, max-age=31536000, immutable` and `ETag: "{hash}"`. A changed icon gets a new URL.
- `/icon` and `/picture` without `v` work for **public** Actors only; anything else returns `404 actor-not-found`. `/icon` returns JSON (`{ "data": { "iconWebp": "..." } }`, with the icon as a base64 string). `/picture` returns the raw image bytes, cached for 5 minutes.
- An Actor without an icon returns `404 picture-not-found`.

```bash
curl -o icon.webp https://api.scrape.ly/v2/actors/janedoe~my-actor/picture
```

```api-test
{ "method": "GET", "path": "/v2/actors/janedoe~my-actor/icon" }
```

## Update Actor

```
PUT /v2/actors/{actorId}
```

Owner only. Same fields as create (all optional), plus:

| Body field | Type | Description |
| --- | --- | --- |
| `actorPermissionLevel` | string | `LIMITED_PERMISSIONS` or `FULL_PERMISSIONS`. Cannot change after the Actor is public. |
| `taggedBuilds` | object | Map of tag → `{ "buildId": "..." }`; `null` removes the tag. The target must be a `SUCCEEDED` build of this Actor, else `404 not-found` (no such build) or `400 invalid-input`. Responses return `{ "buildId", "buildNumber", "finishedAt" }` per tag. Owner only. |
| `defaultNonOwnerBuild` | string \| null | The [User Build](https://scrape.ly/docs/publishing.md#user-build): the build tag or number every other user runs. Must be a `SUCCEEDED` build, else `400 invalid-input`. A version number such as `0.1` is saved as the build number it resolves to, so later builds of that version don't reach users. Empty or `null` clears it, so users run `defaultRunOptions.build`. Can change any time, also after the Actor is public. |
| `isDeprecated` / `isUnderMaintenance` | boolean | Lifecycle flags. |
| `userMaintenanceBadge` / `userDeprecatedBadge` | boolean | Display-only badges. |
| `allowsAgenticUsers` | boolean | Whether AI agents may find the Actor in the [agent Store format](https://scrape.ly/docs/api-store.md#store-for-ai-agents) and run it with agent payments (x402). Defaults to `true`. Non-boolean values return `400 invalid-input`. Returned on `GET /v2/actors/{actorId}`. |

Publishing (`isPublic: true`) requires a name, a description, a successful build, an input schema on the [User Build](https://scrape.ly/docs/publishing.md#user-build) (the default build when none is set), and a Scrape.ly base image (Node.js 20, 22, 24, or 26, or Python 3.11 or newer). A request that doesn't meet these returns `400 publish-validation`.

To make the source of a hidden Actor public, send the Actor name as confirmation. You can hide it again at any time with `{ "hideSourceFiles": true }`.

```bash
curl -X PUT https://api.scrape.ly/v2/actors/janedoe~my-actor \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "hideSourceFiles": false, "confirmPublicSourceFiles": "my-actor" }'
```

## Delete Actor

```
DELETE /v2/actors/{actorId}
```

Owner only. Returns `204 No Content`.

```bash
curl -X DELETE https://api.scrape.ly/v2/actors/eLN5rTB8sYTBWdfzx \
  -H "Authorization: Bearer $SCRAPELY_TOKEN"
```

## Saved input and run options (removed)

```
GET|PUT /v2/actors/{actorId}/input
GET|PUT /v2/actors/{actorId}/run-options
```

These endpoints were removed and answer `410 endpoint-removed`. Your saved input values and run options for an Actor are [saved inputs](https://scrape.ly/docs/api-tasks.md) now: list them with [`GET /v2/actors/{actorId}/saved-inputs`](https://scrape.ly/docs/api-tasks.md#saved-inputs-for-an-actor) (your default saved input has `isDefault: true`) and read or change one with the [task endpoints](https://scrape.ly/docs/api-tasks.md).

## Start a run

```
POST /v2/actors/{actorId}/runs
```

Starts a run and returns `201` with the run object, or `202` if the run is queued. Both set `Location` to `/v2/actor-runs/{runId}`. The JSON body **is the input object at the top level**. If the body isn't a JSON object, the run starts without input.

Any authenticated user can run a public Actor; private Actors are owner-only. Full-permission Actors may need [approval](#permission-approvals).

| Query param | Type | Description |
| --- | --- | --- |
| `build` | string | A build ID, tag, version (such as `0.1`), or build number. A tag or version picks the newest matching build; builds started in the same second go to the higher build number. Owner only: for other users `build` is ignored and the run uses the [User Build](https://scrape.ly/docs/publishing.md#user-build). Default: the Actor's `defaultRunOptions.build`, or `latest`. |
| `memory` | number | Memory in MB. A power of two within your account limits. Default: the Actor's default, or `2048`. |
| `timeout` | number | Timeout in seconds. Default: the Actor's default, or `3600`. |
| `maxItems` | number | Max dataset items. |
| `maxTotalChargeUsd` | number | Max pay-per-event charge in USD. Default: the Actor's `minimalMaxTotalChargeUsd`, if set. |
| `restartOnError` | boolean | Restart the container on error. |
| `waitForFinish` | number | Seconds to wait for a terminal status, from `0` to `60`. The wait stops at 45 seconds (see [Waiting for runs and builds](https://scrape.ly/docs/api-overview.md#waiting-for-runs-and-builds)). If the run is still going, you get the current run object. Ignored when the run is queued. |
| `idempotencyKey` | string | Same as the `Idempotency-Key` header. See [Idempotent run starts](#idempotent-run-starts). |
| `forcePermissionLevel` | string | `LIMITED_PERMISSIONS` or `FULL_PERMISSIONS`. |
| `webhooks` | string | Base64-encoded JSON array of ad-hoc webhook definitions. |

You can also put a `webhooks` array in the JSON body. Ad-hoc webhooks are bound to this run, count toward your webhook limit while it is active, and are deleted after they fire. See [Ad-hoc webhooks](https://scrape.ly/docs/integrations.md#ad-hoc-webhooks).

A queued `202` response keeps the run under `data` and adds queue details:

```json
{
  "data": { "id": "a7XyZ8kLmN2pQ4rSt", "status": "READY" },
  "meta": {
    "queuePosition": 3,
    "estimatedWaitSecs": 45,
    "reason": "concurrency"
  }
}
```

| Status | Type | Meaning |
| --- | --- | --- |
| 400 | `invalid-input` | The input doesn't match the Actor's input schema. See [Input validation](#input-validation). |
| 400 | `no-successful-build` | Your Actor has no successful build to run. |
| 400 | `actor-not-available` | Someone else's Actor can't be run right now. |
| 400 | `invalid-memory` / `invalid-timeout` | Memory is not a power of two within your limits, or the timeout is not a positive integer. |
| 400 | `invalid-max-total-charge` | `maxTotalChargeUsd` is not a valid amount. |
| 400 | `limit-exceeded` | The run would exceed your total memory limit. |
| 400 | `unsupported-pricing` | The Actor uses a pricing model that can no longer be run. |
| 402 | `payment-required` | Not enough credit to reserve for the run. |
| 403 | `forbidden` | Private Actor you don't own. |
| 403 | `full-permission-actor-not-approved` | See [Permission approvals](#permission-approvals). |
| 409 | `idempotency-key-in-progress` | A start with the same `Idempotency-Key` is still in progress. Retry shortly. |
| 422 | `idempotency-key-reused` | The `Idempotency-Key` was already used for a different request (another Actor, task, or build target). |
| 429 | `concurrency-limit-exceeded` / `run-capacity-check-busy` | You're at your concurrent run limit, or another run request from your account is being evaluated. Retry shortly. |

```bash
curl -X POST "https://api.scrape.ly/v2/actors/janedoe~my-actor/runs?memory=2048&timeout=3600" \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "maxResults": 50}'
```

The returned run's `options` include `build` (only on runs of your own Actor), `timeoutSecs`, `memoryMbytes`, `diskMbytes` (twice the memory), `maxItems`, `maxTotalChargeUsd`, and `restartOnError`, the same as [Get run](https://scrape.ly/docs/api-runs.md#get-run) returns.

Secret input fields are encrypted before the run is stored. See [Secret input fields](https://scrape.ly/docs/security.md#secret-input-fields).

To inspect, abort, or fetch logs for the returned `id`, see [Actor runs](https://scrape.ly/docs/api-runs.md).

### Input validation

Before a run starts, its input is checked against the input schema of the build it runs. Schema defaults are filled in first. Input that doesn't match returns `400 invalid-input`, and no run is created or billed. The same check applies to [synchronous runs](#run-actor-synchronously), [task runs](https://scrape.ly/docs/api-tasks.md#run-task) and [scheduled runs](https://scrape.ly/docs/api-schedules.md). A rejected scheduled run is recorded in the schedule log.

```json
{
  "error": {
    "type": "invalid-input",
    "message": "Input is not valid: Field input.maxItems must be >= 1",
    "data": {
      "errors": [{ "field": "input.maxItems", "message": "must be >= 1" }]
    }
  }
}
```

The check covers required fields, types, `enum`, `minimum`/`maximum`, `minLength`/`maxLength`, `pattern`, and array `minItems`/`maxItems` and `items`. Input keys that the schema doesn't list are allowed, unless the schema sets `"additionalProperties": false`. An optional field may be `null`. Input that isn't a JSON object, and Actors without an input schema, are not checked.

To check input without starting a run, use [Validate input](#validate-input).

### Validate input

```
POST /v2/actors/{actorId}/validate-input
```

Validates the JSON body against the input schema, using the same rules as a run start, and starts nothing. It returns `200 { "valid": true }`, or `400 invalid-input` with the field errors shown above. Use the `build` query parameter to pick the build (owner only, like on [Start a run](#start-a-run)). Other users always validate against the [User Build](https://scrape.ly/docs/publishing.md#user-build). An unknown build returns `404 record-not-found`.

```bash
curl -X POST "https://api.scrape.ly/v2/actors/janedoe~my-actor/validate-input" \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "maxResults": 50}'
```

The clients call this endpoint from `client.actor(id).validateInput(input)` (JavaScript) and `client.actor(id).validate_input(input)` (Python).

### Idempotent run starts

A start request can time out on the network after the run was created. Retrying it then starts a second run, which is billed too. To make retries safe, send an `Idempotency-Key` header (or the `idempotencyKey` query parameter) with a value you generate once per intended run:

```bash
curl -X POST "https://api.scrape.ly/v2/actors/janedoe~my-actor/runs" \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Idempotency-Key: nightly-crawl-2026-08-14" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'
```

- The key is 1 to 255 printable ASCII characters, without spaces. Anything else returns `400 invalid-input`.
- Keys are scoped to your account and remembered for 24 hours after the run starts.
- A repeated request with the same key returns `201` with the original run instead of starting a new one. `waitForFinish` works on the repeat too. The body and query parameters of the repeat are ignored.
- If the first request is still starting the run, the repeat gets `409 idempotency-key-in-progress`. Retry after a moment.
- Reusing a key for a different request (another Actor, task, or build target) returns `422 idempotency-key-reused`.
- If the first request fails before a run exists (a validation error, a limit, or no balance), the key is released, so a retry with the same key can start the run.

Without the header, every request starts a new run, so parallel starts of the same Actor and input all run.

The header is also read on [`POST /v2/actor-tasks/{taskId}/runs`](https://scrape.ly/docs/api-tasks.md#run-task) and [`POST /v2/actors/{actorId}/builds`](#start-a-build):

- A task-run replay returns the original run at once and ignores `waitForFinish`.
- A build replay returns `201` with `{ "data": build }`, without `meta` and without waiting.
- On GitHub `push` deliveries the key is ignored.

The JavaScript client retries `5xx` responses on its own. To send the header from it, use the [`requestInterceptors`](https://scrape.ly/docs/client-js.md) option.

## Run Actor synchronously

```
POST /v2/actors/{actorId}/run-sync
GET  /v2/actors/{actorId}/run-sync
```

Starts a run and waits up to **45 seconds** for it to finish (see [Waiting for runs and builds](https://scrape.ly/docs/api-overview.md#waiting-for-runs-and-builds)). On success, returns `200 OK` with the **`OUTPUT` key-value record as raw JSON**, not the `{ "data": ... }` envelope and not the run object. If the record is missing, the body is `{}`.

- `POST`: the body is the input object, as for [Start a run](#start-a-run).
- `GET`: starts a run **without** input.
- `outputRecordKey` selects a different key-value store record (default `OUTPUT`).
- Takes the same query parameters as [Start a run](#start-a-run) except `waitForFinish`. `maxTotalChargeUsd` has no default here.
- The run's own timeout is capped at 300 seconds. Without `timeout`, it's the Actor's default timeout or 300, whichever is lower.

| Status | Type | Meaning |
| --- | --- | --- |
| 200 | — | The run succeeded. The body is the output record. |
| 400 | `run-failed` | The run ended in a status other than `SUCCEEDED`. |
| 408 | `run-timeout-exceeded` | The run did not finish within 45 seconds. It keeps going; the message includes its run ID. |

The same start errors as [Start a run](#start-a-run) also apply.

On a `408`, don't call `run-sync` again: that starts another run. Take the run ID from the message and wait with [GET /v2/actor-runs/{runId}?waitForFinish=60](https://scrape.ly/docs/api-runs.md#get-run), then read its `OUTPUT` record or dataset. For jobs that regularly take longer than 45 seconds, start the run asynchronously instead.

```bash
curl -X POST https://api.scrape.ly/v2/actors/janedoe~my-actor/run-sync \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'
```

### Get dataset items synchronously

```
POST /v2/actors/{actorId}/run-sync-get-dataset-items
GET  /v2/actors/{actorId}/run-sync-get-dataset-items
```

Same wait rules as `run-sync` (max 45 seconds, then `408`). On success, returns `200 OK` with the run's **dataset items** (a JSON array by default, not wrapped in `data`).

It accepts every parameter of [Get dataset items](https://scrape.ly/docs/api-datasets.md), such as `format`, `fields`, `omit`, `offset`, `limit`, and `desc`, so `?format=csv` returns CSV. Responses include the `X-Apify-Pagination-Offset`, `X-Apify-Pagination-Limit`, `X-Apify-Pagination-Count`, `X-Apify-Pagination-Total`, and `X-Apify-Pagination-Desc` headers.

`GET` starts a run without input; `POST` sends the input object as the body.

```bash
curl -X POST "https://api.scrape.ly/v2/actors/janedoe~my-actor/run-sync-get-dataset-items?format=csv" \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'
```

## List Actor runs

```
GET /v2/actors/{actorId}/runs
```

**Your** runs of this Actor. Works for public Actors you don't own too, but only lists your own runs. [Pagination](https://scrape.ly/docs/api-overview.md#pagination) (default `limit` `1000`) plus:

| Query param | Description |
| --- | --- |
| `status` | One status or a comma-separated list, for example `RUNNING,SUCCEEDED`. Use `TIMED-OUT` (hyphen). |
| `startedAfter` / `startedBefore` | ISO 8601 timestamps. |
| `sortBy` | `startedAt` (default), `finishedAt`, `status`, `duration`, or `usage`. |

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actors/eLN5rTB8sYTBWdfzx/runs?status=SUCCEEDED&limit=10&desc=1"
```

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/runs?limit=5&desc=1" }
```

To list **all** your runs across Actors, use [`GET /v2/actor-runs`](https://scrape.ly/docs/api-runs.md#list-runs).

## Run statistics

```
GET /v2/actors/{actorId}/runs/stats
```

Summarizes **your** last 100 runs of the Actor in a date window. Works for public Actors and your own private ones.

| Query param | Description |
| --- | --- |
| `startDate` | Start of the window (default: 30 days ago). |
| `endDate` | End of the window (default: now). |

```json
{
  "data": {
    "totalRuns": 42,
    "statusCounts": { "RUNNING": 1, "SUCCEEDED": 38, "FAILED": 2, "ABORTED": 1, "TIMED-OUT": 0 },
    "dailyStatuses": [],
    "metrics": {
      "results": [],
      "duration": [],
      "datasetFields": [],
      "kvStoreSize": [],
      "requestsHandled": []
    },
    "summary": {
      "totalResults": 12840,
      "totalDuration": 5400,
      "maxCpuUsage": 98.5,
      "maxMemoryUsage": 812,
      "totalDatasetItems": 12840,
      "totalRequests": 13010,
      "totalComputeUnits": 1.25,
      "durationPerResult": 0.42
    },
    "dateRange": { "start": "2026-07-15T00:00:00.000Z", "end": "2026-08-14T15:00:00.000Z" }
  }
}
```

Each `metrics` series is a list of `{ runId, startedAt, value }` points.

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/runs/stats" }
```

## Last run

```
GET /v2/actors/{actorId}/runs/last
```

The most recent run **you** started on this Actor. Optional `status` (for example `SUCCEEDED`) and `waitForFinish` (up to 60, the wait stops at 45 seconds). If no run matches, returns `404 run-not-found`.

### Last-run storage shortcuts

These find the last run and read its default storages, so you can get results without looking up a run ID first. It's the simplest way to consume a scheduled Actor's output: point a report at the shortcut and it always reads the newest data.

Add `?status=SUCCEEDED` to skip failed or aborted runs. Without it, the shortcut uses the most recent run whatever its outcome, which may have an empty or partial dataset.

| Endpoint | Description |
| --- | --- |
| `GET /v2/actors/{actorId}/runs/last/log` | Plain-text log. |
| `GET /v2/actors/{actorId}/runs/last/dataset` | Default dataset. |
| `GET /v2/actors/{actorId}/runs/last/dataset/items` | Dataset items. Takes every [Get dataset items](https://scrape.ly/docs/api-datasets.md) parameter, including `format`. |
| `GET /v2/actors/{actorId}/runs/last/key-value-store` | Default key-value store. |
| `GET /v2/actors/{actorId}/runs/last/key-value-store/keys` | List keys (`limit`, default `1000`, and `exclusiveStartKey`). |
| `GET /v2/actors/{actorId}/runs/last/key-value-store/records/{recordKey}` | Get a record (use `OUTPUT` for named output). Missing records return `404 record-not-found`. |
| `GET /v2/actors/{actorId}/runs/last/request-queue` | Default request queue. |
| `GET /v2/actors/{actorId}/runs/last/request-queue/requests` | List requests (`limit`, `offset`). |
| `GET /v2/actors/{actorId}/runs/last/request-queue/head` | Queue head (`limit`, default `100`). |

The storage objects these shortcuts return are summaries, and their item and request counts are not filled in. For real counts, read the storage by ID: `defaultDatasetId` and the others are on the run object.

Read the named output of the last successful run:

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actors/janedoe~my-actor/runs/last/key-value-store/records/OUTPUT?status=SUCCEEDED"
```

Fetch the newest results as CSV:

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actors/janedoe~my-actor/runs/last/dataset/items?status=SUCCEEDED&format=csv" \
  -o latest.csv
```

Handle the `404` in a report or dashboard rather than assuming data always exists.

```api-test
{ "method": "GET", "path": "/v2/actors/janedoe~my-actor/runs/last" }
```

```api-test
{ "method": "GET", "path": "/v2/actors/janedoe~my-actor/runs/last/dataset/items?status=SUCCEEDED&limit=10" }
```

## List tasks for an Actor

```
GET /v2/actors/{actorId}/tasks
```

Returns **your** saved tasks for this Actor, also for public Actors you don't own. Default `limit` is `20`, newest first. See [Actor tasks](https://scrape.ly/docs/api-tasks.md).

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/tasks" }
```

## List Actor webhooks

```
GET /v2/actors/{actorId}/webhooks
GET /v2/acts/{actorId}/webhooks
```

Your webhooks whose condition targets this Actor. Owner only; limited scoped tokens are rejected. Paginated with `limit` (default and max `1000`), `offset`, and `desc` (default `true`). To create or edit webhooks, see [Webhooks](https://scrape.ly/docs/api-webhooks.md).

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/webhooks" }
```

## Versions

A version holds the source of your Actor at a `major.minor` number, for example `0.1`. Builds compile a version into a runnable image, so the usual deployment sequence is: create or update a version, then start a build from it.

Versions are owner only, also for public Actors: only the owner can read, create, update, or delete them, and other users get `403`.

Source types:

| Source type | Where the code comes from |
| --- | --- |
| `SOURCE_FILES` | Files supplied inline in the request |
| `GIT_REPO` | A Git repository URL, cloned at build time |
| `TARBALL` | A tarball URL, downloaded at build time |
| `GITHUB_GIST` | A GitHub gist |

| Endpoint | Description |
| --- | --- |
| `GET /v2/actors/{actorId}/versions` | List versions. |
| `GET /v2/actors/{actorId}/versions/{versionNumber}` | Get one version, including the contents of its `sourceFiles`. Owner only. |
| `POST /v2/actors/{actorId}/versions` | Push a version: `201` + `Location`. If the version number already exists, it is replaced, including its environment variables. |
| `PUT /v2/actors/{actorId}/versions/{versionNumber}` | Update the fields you send. A file sent without `content` keeps its stored content. |
| `DELETE /v2/actors/{actorId}/versions/{versionNumber}` | Delete: `204`. |
| `POST /v2/actors/{actorId}/versions/fork` | Copy a version into a new number. An existing target number returns `409 version-exists`. |
| `GET /v2/actors/{actorId}/versions/{versionNumber}/source-files` | The version including file contents. |
| `PATCH /v2/actors/{actorId}/versions/{versionNumber}/source-files` | Owner only. Delta update: `{ "updatedFiles": [...], "deletedFiles": [...] }`. Returns `{ "data": { "uploaded": n, "deleted": n } }`. |

The versions list returns `{ "data": { "total": number, "items": [...] } }`. It returns every version and does **not** accept `offset` or `limit`.

Push a version with inline source files:

```bash
curl -X POST https://api.scrape.ly/v2/actors/eLN5rTB8sYTBWdfzx/versions \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "versionNumber": "0.1",
    "buildTag": "latest",
    "sourceType": "SOURCE_FILES",
    "sourceFiles": [
      { "name": "main.js", "format": "TEXT", "content": "import { Actor } from \"scrapely\";" },
      { "name": "scrapely.json", "format": "TEXT", "content": "{ \"actorSpecification\": 1 }" }
    ]
  }'
```

Each entry in `sourceFiles` needs `name`, `format`, and `content`. `format` is `TEXT`, `BASE64` (for binary files), or `ZIP` (a base64 zip archive that the server unpacks into individual files).

Size limits for uploaded source:

- The JSON request body can be at most 10 MB.
- All `ZIP` entries of one request together may unpack to at most 10,000 files and 48 MB. Past that, the request returns `413 payload-too-large`. Use a Git repository or a tarball URL for larger sources.
- A tarball URL can be up to 500 MB.
- The source a version stores, whatever its type, can hold at most 10,000 files, 128 MB in total, and 64 MB per file; a larger one returns `payload-too-large`. Keep large data out of the Actor's folder: download it in the Dockerfile or at run time.

A read-modify-write is safe: `GET` a version, change a field such as `envVars`, and `PUT` the object back, and the source stays as it was. `PUT` replaces the whole file list when you send `sourceFiles`, so leave out the files you want to delete. If every file in the list arrives with an empty `content`, the stored contents are kept; to empty a single file, send it next to files that have content.

Point a version at a Git repository instead:

```bash
curl -X POST https://api.scrape.ly/v2/actors/eLN5rTB8sYTBWdfzx/versions \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "versionNumber": "0.2",
    "buildTag": "latest",
    "sourceType": "GIT_REPO",
    "gitRepoUrl": "https://github.com/janedoe/my-actor#main"
  }'
```

For a private repository, add a [deployment key](#deployment-keys).

If your GitHub integration is connected, linking a version to a GitHub repository also adds a push webhook to that repository, so each push to the linked branch rebuilds the version. This applies to `POST /v2/actors`, `PUT /v2/actors/{actorId}` with `versions`, `POST .../versions`, and a `PUT .../versions/{versionNumber}` that changes `sourceType`, `gitRepoUrl`, or `gitBranch`. Requests made with scoped tokens don't register webhooks. Registration is best effort: the version is saved even if GitHub refuses the webhook. See [CI webhooks](#ci-webhooks).

Once a version has many files, replacing all of them on every deploy is wasteful. `PATCH .../source-files` applies a delta instead:

```bash
curl -X PATCH https://api.scrape.ly/v2/actors/eLN5rTB8sYTBWdfzx/versions/0.1/source-files \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "updatedFiles": [
      { "name": "main.js", "format": "TEXT", "content": "// updated" }
    ],
    "deletedFiles": ["old-helper.js"]
  }'
```

Fork body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `newVersionNumber` | string | Yes | `major.minor`, for example `"1.0"`. |
| `sourceVersionNumber` | string | No | Version to copy (defaults to the first version). |
| `copyEnvVars` | boolean | No | Copy non-secret env vars (default `false`). |

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/versions" }
```

## Environment variables

Environment variables belong to a **version**, not to the Actor, so a variable added to `0.1` is not visible to `0.2`. They are injected into runs, and into builds when `applyToBuild` is set.

| Body field | Type | Description |
| --- | --- | --- |
| `name` | string | Required. Variable name. |
| `value` | string | Required. Variable value. |
| `isSecret` | boolean | Encrypt at rest and hide the value in API responses. |
| `applyToBuild` | boolean | Also inject during the Docker build, not just at run time. |

Once `isSecret` is set, the value is never returned again: responses show `"value": ""`. Store the plaintext in your own secret manager if you need to read it back.

Any signed-in user can list the environment variables of a **public** Actor and read the non-secret values. Mark every credential as secret.

Set `applyToBuild` only for values the build itself needs, such as a private registry token. A runtime credential does not belong in the build, where it can end up in an image layer.

| Endpoint | Description |
| --- | --- |
| `GET /v2/actors/{actorId}/versions/{versionNumber}/env-vars` | List. |
| `POST /v2/actors/{actorId}/versions/{versionNumber}/env-vars` | Create or overwrite a variable by name: `201`. Body: `{ "name", "value", "isSecret", "applyToBuild" }`. |
| `GET /v2/actors/{actorId}/versions/{versionNumber}/env-vars/{envVarName}` | Get one, or `404 env-var-not-found`. |
| `PUT /v2/actors/{actorId}/versions/{versionNumber}/env-vars/{envVarName}` | Update. |
| `DELETE /v2/actors/{actorId}/versions/{versionNumber}/env-vars/{envVarName}` | Delete: `204`. |

```bash
curl -X POST https://api.scrape.ly/v2/actors/eLN5rTB8sYTBWdfzx/versions/0.1/env-vars \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "MY_TOKEN", "value": "secret-value", "isSecret": true }'
```

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/versions/0.1/env-vars" }
```

## Builds

A build turns a version into a runnable image. Builds are addressed two ways: under an Actor (`/v2/actors/{actorId}/builds`) to list and start them, and under `/v2/actor-builds/{buildId}` to work with one build.

Builds, build logs, and build OpenAPI documents are **owner only**, also for public Actors. Everyone else gets `403` on the `/v2/actors/{actorId}/builds…` routes and `404 build-not-found` on the `/v2/actor-builds/{buildId}…` routes, the same as for a build that doesn't exist. Other users run the [User Build](https://scrape.ly/docs/publishing.md#user-build) and read its schemas from [Get Actor](#get-actor).

| Endpoint | Description |
| --- | --- |
| `GET /v2/actors/{actorId}/builds` | List builds for an Actor. Owner only. Default `limit` `1000`. Oldest first by default; `desc=1` returns newest first. Builds started in the same second are ordered by build number. |
| `GET /v2/actors/{actorId}/builds/preflight` | Dockerfile checks before building. Query `version` required. Owner only. |
| `POST /v2/actors/{actorId}/builds` | Start a build. Owner only; needs a positive balance. |
| `GET /v2/actors/{actorId}/builds/default` | The build tagged `latest`, else the newest succeeded build, else the newest build of any status. Doesn't follow `defaultRunOptions.build`. Owner only. Optional `waitForFinish` (up to 60, the wait stops at 45 seconds). `404 build-not-found` only if the Actor has no builds. |
| `GET /v2/actors/{actorId}/builds/{buildId}/openapi.json` | OpenAPI document for a build. `buildId` may be `default`. Owner only. |
| `GET /v2/actor-builds` | List your builds across Actors. Default `limit` `1000`. Oldest first by default; `desc=1` for newest first. |
| `GET /v2/actor-builds/{buildId}` | Get a build. Owner only. Optional `waitForFinish` (up to 60, the wait stops at 45 seconds). |
| `GET /v2/actor-builds/{buildId}/openapi.json` | OpenAPI document for a build. Owner only. |
| `GET /v2/actor-builds/{buildId}/log` | Build log as **plain text**. Owner only. |
| `POST /v2/actor-builds/{buildId}/abort` | Abort a running build. Owner only. Returns the build under `data`. |
| `DELETE /v2/actor-builds/{buildId}` | Delete a build: `204`. Owner only. The current [User Build](https://scrape.ly/docs/publishing.md#user-build) can't be deleted (`400 invalid-input`); set a different one first. |

### Start a build

| Query param | Type | Required | Description |
| --- | --- | --- | --- |
| `version` | string | Yes | Version number to build. |
| `tag` | string | No | Build tag (defaults to the version's `buildTag`, or `latest`). |
| `waitForFinish` | number | No | Seconds to wait, up to 60. The wait stops at 45 seconds. Ignored when the build is queued. |
| `idempotencyKey` | string | No | Same as the `Idempotency-Key` header. A repeat with the same key returns `201` with the original build, without `meta` and without waiting. See [Idempotent run starts](#idempotent-run-starts). |
| `syncGitFromRemote` | boolean | No | Pull the linked Git repo before building. The build then reads `actor.json`, the input schema, and the other schemas from the pulled commit, so they always match the code it builds. |

Optional JSON body: `{ "options": { "memoryMbytes", "timeoutSecs", "noCache" } }`.

- `memoryMbytes` defaults to 4096 MB, or your account's build memory limit if that is lower. It must be a power of two, within your limit, and small enough to fit on a single worker.
- `timeoutSecs` defaults to your account's maximum build timeout and cannot exceed it.

An immediately dispatched build returns `201`; a queued build returns `202`. Both include `meta.warnings`, `meta.hasWarnings`, and `meta.guidance` from the Dockerfile preflight. A queued response also includes `meta.queuePosition`, `meta.estimatedWaitSecs`, `meta.timeoutAt`, and `meta.reason`.

```json
{
  "data": { "id": "bUiLdIdExAmPlE123", "status": "READY" },
  "meta": {
    "warnings": [],
    "hasWarnings": false,
    "guidance": "Review the Dockerfile guidance before rebuilding.",
    "queuePosition": 2,
    "estimatedWaitSecs": 30,
    "timeoutAt": 1786725000000,
    "reason": "No server has sufficient capacity. Requested: 2048MB"
  }
}
```

The `Location` header points to `/v2/actors/{actorId}/builds/{buildId}`, which has no `GET` handler. Read the build from `GET /v2/actor-builds/{buildId}` instead, using `data.id`.

| Status | Type | Meaning |
| --- | --- | --- |
| 400 | `invalid-input` | `version` is missing or unknown, or the memory or timeout is invalid. |
| 400 | `limit-exceeded` | Running builds and runs would exceed your total memory limit. |
| 402 | `insufficient-balance` | Your balance is not positive. |
| 403 | `forbidden` | You don't own the Actor. |
| 409 | `idempotency-key-in-progress` | A build with the same `Idempotency-Key` is still being started. Retry shortly. |
| 422 | `idempotency-key-reused` | The `Idempotency-Key` was already used for a different request (another Actor, task, or build target). |
| 429 | `concurrent-builds-limit-exceeded` | You are at your concurrent build limit. |

Requests that carry an `X-GitHub-Event` header other than `push` are acknowledged with `200` and `{ "data": { "ignored": true, "event": "..." } }`, and no build starts.

A `push` event only builds when it matches the version. Otherwise the response is `200` with `{ "data": { "ignored": true, "event": "push", "reason": "...", "ref": "refs/heads/..." } }`:

| `reason` | Meaning |
| --- | --- |
| `repo-not-linked` | The push came from a different repository than the version's `gitRepoUrl`. Renamed repositories still match. |
| `ref-not-linked` | The push updated a branch or tag other than the version's branch (the `#branch` in `gitRepoUrl`, or `gitBranch`, or else the repository's default branch). |
| `ref-deleted` | The push deleted the branch. |
| `source-not-git` | The version isn't built from a Git repository. |
| `duplicate-push` | GitHub delivered the same push again. `buildId` is the build the first delivery started. |

Requests without an `X-GitHub-Event` header, such as a `curl` from GitHub Actions, aren't filtered. See [CI webhooks](#ci-webhooks).

Start a build and capture the ID:

```bash
BUILD_ID=$(curl -sS -X POST \
  "https://api.scrape.ly/v2/actors/eLN5rTB8sYTBWdfzx/builds?version=0.1&tag=latest" \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" | jq -r '.data.id')
```

Because the build may be queued rather than dispatched, do not assume `201`. Treat `202` as success and poll `GET /v2/actor-builds/{buildId}?waitForFinish=60` until the status is terminal. Each wait stops at 45 seconds; a longer build still finishes, the response just returns first.

Run `GET /v2/actors/{actorId}/builds/preflight?version=0.1` first to catch Dockerfile problems before spending build minutes.

Files under `/tmp` don't survive the build's layer cache. To keep rebuilds working, the step that first copies into `/tmp` (`COPY` or `ADD`) and every step after it in that stage always rebuild, so those steps never come from the cache. A `RUN` step that writes into `/tmp` for a later `RUN` isn't covered: the later step can fail on a cached rebuild. Copy build inputs somewhere else, such as `/usr/src/app`, and do the whole `/tmp` job inside one `RUN`.

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/builds?limit=5&desc=1" }
```

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/builds/default" }
```

```api-test
{ "method": "GET", "path": "/v2/actor-builds/{buildId}" }
```

### Get build log

```
GET /v2/actor-builds/{buildId}/log
```

Returns the build log as plain text (`text/plain; charset=utf-8`). The stored log keeps the newest 1 MB of output; older lines are dropped with a note. Build logs are kept for 3 days after the build finishes.

A build that prints more than 64 MB of output is stopped and fails with `[Scrapely Build] Error: the build printed more than 64 MB of output and was stopped.` Make noisy `RUN` steps quieter, for example `npm install --loglevel=warn` or `pip install -q`.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actor-builds/$BUILD_ID/log"
```

```api-test
{ "method": "GET", "path": "/v2/actor-builds/{buildId}/log" }
```

### OpenAPI document

```
GET /v2/actors/{actorId}/openapi.json
```

The OpenAPI document for the build the caller runs: the default build for the owner, the [User Build](https://scrape.ly/docs/publishing.md#user-build) for everyone else. Public Actors need no token; private Actors need the owner. Non-owners get `info.version` `1.0.0` instead of the build's version. If the Actor has no build yet, the owner gets `404 build-not-found` and everyone else `404 actor-not-available`.

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/openapi.json" }
```

The `openapi.json` endpoints return a minimal OpenAPI 3.0 document with the Actor's title, description, and version. The request and response schemas are generic objects, not the Actor's input schema, so the document is not enough to generate a typed client. Read the input schema from [Get Actor](#get-actor) instead.

Build statuses: `READY`, `RUNNING`, `SUCCEEDED`, `FAILED`, `TIMED-OUT`, `ABORTED`. A run can only start from a `SUCCEEDED` build.

### Browser patch

```
GET /v2/actors/{actorId}/browser-patch
PUT /v2/actors/{actorId}/browser-patch
```

The per-Actor switch for the [Scrape.ly browser](https://scrape.ly/docs/browser.md), shown in the dashboard as **Settings** → **Scrapely Browser** → **Use Scrapely browser**. Owner only.

| Item | Value |
| --- | --- |
| Response | `{ "data": { "browserPatchEnabled": false } }` |
| `PUT` body | `{ "browserPatchEnabled": true }`. A non-boolean returns `400 invalid-input` |
| Default | `false`: builds keep their browser binaries and the Actor launches its own browsers |
| Takes effect | On the next build. Each build stores the value as `browserPatched`, and runs follow their build. Legacy builds with `browserPatched: null` count as `true` |

- **On:** the build removes Playwright, pyppeteer, and Puppeteer browser downloads and redirects every launch to the Scrape.ly browser. Your own proxy URLs are ignored for browser traffic.
- **Off:** runs get `SCRAPELY_BROWSER_PATCH=0`. `CDP_URL` is still set for Actors that connect over CDP themselves.
- Your stored source is never modified. The setting is independent of the account-level **Transform actors** setting.

```bash
curl -X PUT -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"browserPatchEnabled": true}' \
  "https://api.scrape.ly/v2/actors/eLN5rTB8sYTBWdfzx/browser-patch"
```

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/browser-patch" }
```

## CI webhooks

Trigger a build automatically when you push to Git. With the GitHub integration connected, linking a version to a GitHub repository registers the webhook for you (see [Versions](#versions)), so you only need these endpoints to set it up by hand, to repair an older webhook, or to call the build from GitHub Actions. Both endpoints require a user API key and work only for Actors you own. You can pass the Actor ID or `username~actor-name`; the webhook is always keyed by the Actor ID.

### Get the webhook URL

```
GET /v2/actors/{actorId}/ci/webhook-url
```

Returns a ready-to-POST build URL plus setup instructions for GitHub webhooks and GitHub Actions. The URL carries a per-Actor CI secret, which only works for starting builds of this Actor, so you don't have to put your API key in CI. It builds with `tag=latest`, `waitForFinish=0`, and `syncGitFromRemote=true`. Treat it as a secret.

Optional query `version`: must match an existing version. If you leave it out, a `GIT_REPO` version is preferred, otherwise the first version.

| Response field | Description |
| --- | --- |
| `webhookUrl` | The build URL to call with `POST`. |
| `actorId` | The Actor ID. |
| `defaultVersion` | The version the URL builds (`0.0` if the Actor has no versions). |
| `buildTag` | The tag the build gets. |
| `instructions` | Setup steps for GitHub. |

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actors/eLN5rTB8sYTBWdfzx/ci/webhook-url?version=0.1"
```

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/ci/webhook-url" }
```

### Register the GitHub webhook

```
POST /v2/actors/{actorId}/ci/webhook
```

Registers the build webhook on the version's GitHub repository, or repairs the webhook that is already there. The Actor needs a `GIT_REPO` version on GitHub, and your account must have the GitHub integration connected. The body is optional:

| Body field | Type | Description |
| --- | --- | --- |
| `version` | string | Optional. The version to build. Defaults to the first `GIT_REPO` version on GitHub. |
| `repoFullName` | string | Optional. `owner/repo`. Defaults to the version's repository and must match it if you send it. |

A webhook that already points at this Actor and version is reused. If it's disabled, isn't sent as JSON, skips TLS verification, or carries an old token (such as your API key), it is updated in place, and `updated` is `true`.

Returns `201` with `{ "data": { "webhookUrl", "repoFullName", "version", "hookId", "alreadyExisted", "updated" } }`.

```bash
curl -X POST -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" -d '{}' \
  "https://api.scrape.ly/v2/actors/eLN5rTB8sYTBWdfzx/ci/webhook"
```

| Status | Type | Meaning |
| --- | --- | --- |
| 400 | `invalid-input` | The Actor has no `GIT_REPO` version, `version` isn't one, the version isn't on GitHub, or `repoFullName` doesn't match its repository. |
| 400 | `github-not-connected` | Connect GitHub in the dashboard first. |
| 422 | `webhook-registration-failed` | GitHub refused the webhook. When `error.data.manualSetupRequired` is `true`, add the URL from `ci/webhook-url` in the repository settings yourself. |

## Deployment keys

```
POST   /v2/actors/{actorId}/versions/{versionNumber}/deployment-keys
GET    /v2/actors/{actorId}/versions/{versionNumber}/deployment-keys
DELETE /v2/actors/{actorId}/versions/{versionNumber}/deployment-keys/{keyId}
```

A deployment key is an SSH key pair that lets Scrape.ly clone a **private Git repository** when it builds a `GIT_REPO` version. Scrape.ly keeps the private key; you add the returned `publicKey` to the repository as a read-only deploy key (on GitHub: **Settings → Deploy keys**).

Owner only, and requires a user API key.

- `POST` creates a key and returns `201` with `{ "data": { "id", "publicKey", "title", "createdAt" } }`. Optional body: `{ "title": "CI" }`.
- `GET` lists the version's keys.
- `DELETE` returns `204`, or `404 key-not-found`.

```api-test
{ "method": "GET", "path": "/v2/actors/{actorId}/versions/0.1/deployment-keys" }
```

## Store listing

```
GET /v2/store/actors
```

The public catalog of published, non-deprecated Actors. Authentication is optional.

| Query param | Type | Description |
| --- | --- | --- |
| `search` | string | Full-text search. |
| `category` | string | Store category. |
| `username` | string | Only Actors published by this user. |
| `sortBy` | string | `popularity` (default), `newest`, `rating`, `users`, `updated`, or `name`. `relevance` and `lastUpdate` are accepted as aliases for `popularity` and `updated`. |
| `desc` | boolean | Reverse the sort order. |
| `pricingModel` | string | `free` for free Actors, or `PAY_PER_EVENT`. |
| `excludeKnown` | boolean | Leave out Actors you created or already ran. Needs a token. |
| `includeUnrunnableActors` | boolean | Accepted for compatibility; it has no effect. |
| `limit` / `offset` | number | Pagination. Default `limit` is `20`, **max 100**. |

```bash
curl "https://api.scrape.ly/v2/store/actors?limit=20&sortBy=popularity"
```

```api-test
{ "method": "GET", "path": "/v2/store/actors?limit=5" }
```

Reviews and issues: [Store, reviews & issues](https://scrape.ly/docs/api-store.md).

## Permission approvals

An Actor with `actorPermissionLevel` `FULL_PERMISSIONS` can act on your account, so you must approve it before you run it, unless you own it or skip approvals. Until then, run requests fail with `403 full-permission-actor-not-approved`, and `error.data.approvalUrl` links to the approval page.

You can also:

- approve it with `POST /v2/actors/{actorId}/approve-permissions` (below), or
- pass `forcePermissionLevel=LIMITED_PERMISSIONS` to run the Actor with limited permissions, which needs no approval.

A run request can't approve an Actor by itself: a `permissionApproved` query parameter is ignored.

These endpoints require a **user API key**:

| Endpoint | Description |
| --- | --- |
| `POST /v2/actors/{actorId}/approve-permissions` | Approve: `201` if new, `200` if already approved. `{ "data": { "approved": true } }`. |
| `DELETE /v2/actors/{actorId}/approve-permissions` | Revoke: `204`. |
| `GET /v2/users/me/permission-approval-skip` | `{ "data": { "skipActorPermissionApprovals": true } }`. |
| `PUT /v2/users/me/permission-approval-skip` | Body: `{ "skip": true }`. |

```api-test
{ "method": "GET", "path": "/v2/users/me/permission-approval-skip" }
```

## Bookmarks and insights

Bookmark a public Actor with `POST` / `DELETE` / `GET /v2/actors/{actorId}/bookmark`, and list yours with `GET /v2/users/me/bookmarks`. Details: [Search, alerts & utilities](https://scrape.ly/docs/api-misc.md).

Owner monetization and acquisition: [Earnings](https://scrape.ly/docs/api-earnings.md). Debugging stats: [Search, alerts & utilities](https://scrape.ly/docs/api-misc.md).

Actors with standby enabled accept HTTP requests at `/v2/{username}/{actorName}/{path}`. See [Standby mode](https://scrape.ly/docs/api-standby.md).

---

- Previous: [API guide](https://scrape.ly/docs/api.md)
- Next: [Actor runs](https://scrape.ly/docs/api-runs.md)
- All docs: [Docs index](https://scrape.ly/docs.md), [llms.txt](https://scrape.ly/llms.txt)
