---
title: "API keys & permissions · Scrape.ly Docs"
description: "Create many API keys, limit them with scopes, resources, IPs, and spending caps, and revoke them."
url: https://scrape.ly/docs/api-keys
markdown: https://scrape.ly/docs/api-keys.md
---

# API keys & permissions

> Create many API keys, limit them with scopes, resources, IPs, and spending caps, and revoke them.

Your account can have many API keys. Give each app, server, CI pipeline, or AI agent its own key, limit what it can do with scopes, and revoke it on its own without touching the others. Manage keys in the [dashboard](https://my.scrape.ly) under **Settings → API keys**, or over the API with the [key management endpoints](#manage-keys-over-the-api).

Every key is sent the same way, in the `Authorization` header:

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/users/me
```

The API does not read keys from a `?token=` query parameter. The only places that take a token in the URL are the [CI build webhook](https://scrape.ly/docs/api-actors.md#ci-webhooks), which uses its own per-Actor secret, and the [hosted MCP server](https://scrape.ly/docs/mcp.md) at `/mcp`, which passes the key through to the API.

## What a key has

| Field | Description |
| --- | --- |
| Name | Required, up to 100 characters. Shown in the key list and in notifications. |
| Description | Optional, up to 500 characters. Note where the key is used. |
| Created | When the key was created. |
| Last used | The time and IP address of the key's most recent request. |
| Expires | **Never**, or a date. After it, requests with the key get `401 token-expired`. |
| Access | **Full access** or a list of [scopes](#scopes), plus optional [resource restrictions](#resource-restrictions) and [limits](#optional-limits). |
| Status | `active`, `expired`, or `revoked`. |

The secret is shown **once**, when you create the key. Scrape.ly stores only a hash of it, so it can't be shown again. Afterwards the dashboard and the API show a masked prefix such as `scrapely_sk_…ab12`. Copy the key into your secret store right away; if you lose it, regenerate it.

- **Revoke** stops a key immediately. A revoked key stays in the list, marked as revoked, and its requests get `401 token-not-valid`.
- **Regenerate** creates a new key with the same name, description, expiry, and permissions, and revokes the old one at the same time. Nothing is rotated silently: you get the new secret in the response and have to deploy it. A revoked key can't be regenerated; create a new one instead.

## The Default key

Every account has a **Default key**: the account's original API key, in the format `scrapely_` followed by 48 hex characters. It is listed first, with the ID `default`.

- It has full access and never expires. The dashboard uses it, and it keeps working exactly as before.
- Only the Default key can create, edit, regenerate, and revoke API keys. In practice that means the dashboard.
- It can't be edited or revoked. You can rotate it in the dashboard, or with `POST /v2/api-keys/reset` (see [Users API](https://scrape.ly/docs/api-users.md#api-keys)). Rotating it doesn't affect your additional keys.

Additional keys look like `scrapely_sk_` + 16 hex characters + `_` + 48 hex characters, for example `scrapely_sk_3f9a0c1d2e4b5a67_…`.

## Full access or scoped

When you create a key, choose one of two access modes:

| Mode | What the key can do |
| --- | --- |
| **Full access** | Everything the Default key can do, except managing API keys. |
| **Scoped** | Only what its scopes cover. Everything else is refused with `403 insufficient-permissions`. |

Scoped keys are deny-by-default: an endpoint that no scope covers is refused, even when it only reads data.

The dashboard has two presets to start from:

- **Read only**: every `:read` scope.
- **Read & write**: every read and write scope.
- **Automations**: `automations:write`, `runs:read`, `datasets:read`, and `tasks:read` (with `actors:run`), the run permission **Actor's own permission level**, and no Actor limit. Enough for a service or agent that creates and runs automations.

You can then add or remove single scopes.

## Scopes

A write scope includes its read scope. `actors:manage` includes `actors:build`, and `actors:run` and `actors:build` include `actors:read`.

| Scope | Allows |
| --- | --- |
| `actors:read` | Read Actors, versions, builds, source files, and OpenAPI documents. |
| `actors:run` | Start runs of Actors and tasks, including synchronous runs (`run-sync`, `run-sync-get-dataset-items`) and [Standby](https://scrape.ly/docs/api-standby.md) URLs. |
| `actors:build` | Start and abort builds. |
| `actors:manage` | Create, update, and delete Actors and versions, environment variables, deployment keys, and the CI webhook. **Sensitive**: a key with this scope can change the code your other keys run. |
| `runs:read` | List and get runs, logs, run input, and a run's default dataset, key-value store, and request queue through `/v2/actor-runs/{runId}/dataset`, `/key-value-store`, and `/request-queue`. Also the last-run endpoints (`…/runs/last`). |
| `runs:write` | Abort, reboot, update, and delete runs, and write to a run's default storages. Resurrect and metamorph also need `actors:run` and the run permission `actor-default`; with an Actor restriction, the metamorph target must be one of the listed Actors. |
| `tasks:read` | List and get tasks and their input. Tasks are the dashboard's **saved inputs** (the key form calls this area **Saved inputs (tasks)**), so this also covers `GET /v2/actors/{actorId}/saved-inputs`. |
| `tasks:write` | Create, update, and delete tasks (saved inputs). |
| `datasets:read` | List datasets, get them, and read their items. |
| `datasets:write` | Create, update, and delete datasets, and push items. |
| `keyValueStores:read` | List key-value stores, get them, list keys, and read records. |
| `keyValueStores:write` | Create, update, and delete key-value stores and records. |
| `requestQueues:read` | List request queues, get them, and read requests and the queue head. |
| `requestQueues:write` | Create, update, and delete request queues, add and update requests, and lock them. |
| `schedules:read` | List and get schedules and their logs. |
| `schedules:write` | Create, update, and delete schedules. Also needs `actors:run` **and** the run permission `actor-default`, because scheduled runs start later with your account's own permissions, not the key's. Refused, except for deletes, on keys with a spending cap. |
| `automations:read` | List and get [automations](https://scrape.ly/docs/api-automations.md), their run history, and single runs. |
| `automations:write` | Create, update, delete, duplicate, and run automations, AI step included. Also needs `actors:run` **and** the run permission `actor-default`, and is refused for keys limited to specific Actors, because automation runs start with your account's own permissions, not the key's. Refused, except for deletes, on keys with a spending cap: those runs and the AI step, which is billed to your account like the AI chat, would run later without the key's caps. |
| `webhooks:read` | List and get webhooks, webhook dispatches, and the integrations list. |
| `webhooks:write` | Create, update, delete, and test webhooks. Also needs `actors:run`, `runs:read`, and the run permission `actor-default`, because a webhook's integration token can start and read runs. Refused for keys with an Actor or storage restriction or a spending cap. |
| `billing:read` | Usage, limits, plan, and stats: `GET /v2/users/me/usage/*`, `/v2/users/me/limits`, `/v2/users/me/plan`, and `/v2/users/me/stats`. |

### Endpoints outside the scopes

| Who can call it | Endpoints |
| --- | --- |
| **Any valid key** | `GET /v2/users/me` (your identity; this is what `scrapely login` checks) and public Store and catalog reads. |
| **Full-access keys and the Default key** | Profile, notification settings, dashboard notifications, messages, monetization and payouts, earnings, approving full-permission Actors, bookmarks, writing issues and reviews, and connecting Slack or GitHub. |
| **Default key only** | `/v2/api-keys*`, `GET /v2/scrapely`, the AI chat, and the dashboard live feed. |
| **Run tokens only** | The [LLM gateway](https://scrape.ly/docs/llm-gateway.md) and the solver. No API key works there. |

## Example requests by scope

The examples use `$SCRAPELY_TOKEN` for the key. Replace the IDs with your own.

**`actors:read`**: get an Actor.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/actors/{actorId}
```

**`actors:run`**: start a run. The body is the Actor input.

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

**`actors:build`**: build version `0.1`.

```bash
curl -X POST -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actors/{actorId}/builds?version=0.1&tag=latest"
```

**`actors:manage`**: update an Actor.

```bash
curl -X PUT https://api.scrape.ly/v2/actors/{actorId} \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "My scraper"}'
```

**`runs:read`**: read a run's log, and the items of its default dataset.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/actor-runs/{runId}/log

curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/actor-runs/{runId}/dataset/items
```

**`runs:write`**: abort a run.

```bash
curl -X POST -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/actor-runs/{runId}/abort
```

**`tasks:read`**: list your tasks.

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

**`tasks:write`**: create a task.

```bash
curl -X POST https://api.scrape.ly/v2/actor-tasks \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"actId": "{actorId}", "name": "daily-shop", "input": {"url": "https://example.com"}}'
```

**`datasets:read`**: read dataset items.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/datasets/{datasetId}/items
```

**`datasets:write`**: push items.

```bash
curl -X POST https://api.scrape.ly/v2/datasets/{datasetId}/items \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[{"title": "First item"}]'
```

**`keyValueStores:read`**: read a record.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/key-value-stores/{storeId}/records/OUTPUT
```

**`keyValueStores:write`**: write a record.

```bash
curl -X PUT https://api.scrape.ly/v2/key-value-stores/{storeId}/records/CONFIG \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"maxPages": 50}'
```

**`requestQueues:read`**: read the queue head.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/request-queues/{queueId}/head
```

**`requestQueues:write`**: add a request.

```bash
curl -X POST https://api.scrape.ly/v2/request-queues/{queueId}/requests \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/page-2", "uniqueKey": "https://example.com/page-2"}'
```

**`schedules:read`**: list schedules.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/schedules
```

**`schedules:write`** (with `actors:run` and run permission `actor-default`): create a schedule.

```bash
curl -X POST https://api.scrape.ly/v2/schedules \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "daily-scrape",
    "cronExpression": "0 6 * * *",
    "actions": [{ "type": "RUN_ACTOR_TASK", "actorTaskId": "{taskId}" }]
  }'
```

**`automations:read`**: list automations.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/automations
```

**`automations:write`** (with `actors:run` and run permission `actor-default`): run an automation now.

```bash
curl -X POST -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/automations/{automationId}/runs
```

**`webhooks:read`**: list webhooks.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/webhooks
```

**`webhooks:write`** (with `actors:run` and `runs:read`): create a webhook.

```bash
curl -X POST https://api.scrape.ly/v2/webhooks \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "eventTypes": ["ACTOR.RUN.SUCCEEDED"],
    "requestUrl": "https://example.com/hooks/scrapely",
    "condition": { "actorId": "{actorId}" }
  }'
```

**`billing:read`**: this month's usage.

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/users/me/usage/monthly
```

### A refused request

A key with only `datasets:read` that tries to push items:

```bash [Request]
curl -X POST https://api.scrape.ly/v2/datasets/{datasetId}/items \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[{"title": "First item"}]'
```

```json [Response]
{
  "error": {
    "type": "insufficient-permissions",
    "message": "This API key does not have the \"datasets:write\" scope."
  }
}
```

The status is `403`. Nothing is written.

## Resource restrictions

A scoped key can also be limited to specific resources.

### Specific Actors

With a list of Actors, the key can only read, run, and build those Actors, and see their runs, builds, logs, and tasks.

- Account-wide lists are refused, for example `GET /v2/actors` and `GET /v2/actor-runs`. Use the per-Actor endpoints instead, such as `GET /v2/actors/{actorId}/runs`.
- The key can't create Actors, schedules, or webhooks.

You can add your own Actors and public Store Actors, by ID or by `username/actor-name`.

### Specific storages

With a list of storage IDs (datasets, key-value stores, and request queues), the key can only use those storages.

- Listing storages and creating new ones are refused.
- A run's default storages are reachable through `/v2/actor-runs/{runId}/dataset`, `/v2/actor-runs/{runId}/key-value-store`, and `/v2/actor-runs/{runId}/request-queue` when they are on the list, or when the key is also limited to specific Actors and may read that run.

## Run permission

This setting decides how much access a run gets when a scoped key starts it.

| Value | What the run gets |
| --- | --- |
| `limited` (default, recommended) | The run is forced to `LIMITED_PERMISSIONS`. Its run token can only use the run's own default storages, storages passed in its input, storages it creates, and the child runs it starts. The key can't approve full-permission Actors. |
| `actor-default` | The run gets the permission level the Actor declares, which may be `FULL_PERMISSIONS`. Use it for a service that should only start one trusted Actor, ideally together with an [Actor restriction](#specific-actors). |

With `limited`, [Standby](https://scrape.ly/docs/api-standby.md) URLs are refused (a Standby run uses the Actor's own permissions, as on Apify), and so are resurrect and metamorph.

Attaching ad-hoc webhooks to a run start (the `webhooks` body field or query parameter) needs the same scopes as creating a webhook: `webhooks:write` and `runs:read`. Keys limited to specific Actors can't attach them.

Full-access keys behave like the Default key: runs get the Actor's declared permission level. See [Security](https://scrape.ly/docs/security.md#actor-permissions) for what each level allows.

## Optional limits

None of these are set by default.

| Limit | Effect |
| --- | --- |
| **IP allowlist** | IP addresses or CIDR ranges, IPv4 or IPv6, for example `203.0.113.7` or `2001:db8::/32`. Requests from any other address get `403 api-key-ip-not-allowed`. |
| **Max charge per run** | The default and the ceiling of `maxTotalChargeUsd` for every run the key starts. A run asking for more is capped. |
| **Max charge per day** | The total the key's runs may charge per UTC day. Each new run is capped to what's left of the day; a run that is still going holds its whole cap until it finishes and is settled, so parallel starts share one budget. Once it's used up, run starts return `402 api-key-daily-cap-reached` until the next UTC day. |
| **Rate limit per minute** | Requests per minute for this key. Over it, the API returns `429 rate-limit-exceeded` with `Retry-After`. Your [account rate limits](https://scrape.ly/docs/api-overview.md#rate-limits) still apply. |

## Errors

| Status | Type | Meaning |
| --- | --- | --- |
| 401 | `token-not-valid` | The key is unknown or revoked. |
| 401 | `token-expired` | The key's expiry date has passed. |
| 403 | `insufficient-permissions` | The key's scopes or resource restrictions don't cover this request. |
| 403 | `api-key-ip-not-allowed` | The request came from an address outside the key's IP allowlist. |
| 402 | `api-key-daily-cap-reached` | The key's daily charge limit is used up, so the run can't start. |
| 429 | `rate-limit-exceeded` | The key's or the account's rate limit was hit. Wait for `Retry-After` seconds. |

## Manage keys over the API

These endpoints accept **only the Default key**. Any other key, a run token, or an OAuth token gets `403 insufficient-permissions`. They are limited to 10 requests per minute per IP address.

```
GET    /v2/api-keys
POST   /v2/api-keys
GET    /v2/api-keys/scopes
GET    /v2/api-keys/{keyId}
PUT    /v2/api-keys/{keyId}
PATCH  /v2/api-keys/{keyId}
DELETE /v2/api-keys/{keyId}
POST   /v2/api-keys/{keyId}/regenerate
```

| Endpoint | Description |
| --- | --- |
| `GET /v2/api-keys` | Lists every key, the Default key first (ID `default`), with masked prefixes. Revoked keys are included. |
| `POST /v2/api-keys` | Creates a key. Returns `201`; the response includes `key` once. |
| `GET /v2/api-keys/scopes` | The scope catalog: `scopes`, the read-only preset (`readOnly`), what each write scope `implies`, and the `requirements` of `automations:write`, `schedules:write`, and `webhooks:write` (`alsoRequires`, `runPermission`, `allActors`). The dashboard uses it to warn about a key that would be refused. |
| `GET /v2/api-keys/{keyId}` | One key. |
| `PUT` / `PATCH /v2/api-keys/{keyId}` | Changes a key's name, description, expiry, or permissions. The secret stays the same. The Default key can't be edited. |
| `DELETE /v2/api-keys/{keyId}` | Revokes a key immediately. Returns `204`. The Default key can't be revoked. |
| `POST /v2/api-keys/{keyId}/regenerate` | Creates a new key with the same settings and revokes the old one. Returns `201`; the response includes `key` once and `replaces` with the old key's ID. |

Request body fields for create and update:

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Required on create. Up to 100 characters. |
| `description` | string or null | Up to 500 characters. |
| `expiresAt` | string or null | ISO 8601 date and time, or `null` for no expiry. |
| `fullAccess` | boolean | `true` for full access. Scopes and restrictions are then ignored. |
| `scopes` | string[] | Scopes for a scoped key. Implied scopes are added for you. |
| `actorIds` | string[] | Limit the key to these Actors (IDs or `username/actor-name`). |
| `storageIds` | string[] | Limit the key to these dataset, key-value store, and request queue IDs. |
| `ipAllowlist` | string[] | IP addresses or CIDR ranges. |
| `runPermission` | string | `limited` (default) or `actor-default`. |
| `maxChargePerRunUsd` | number or null | Default and ceiling of `maxTotalChargeUsd` for runs the key starts. |
| `maxChargePerDayUsd` | number or null | Total charges of the key's runs per UTC day. |
| `rateLimitPerMin` | number or null | Requests per minute for this key. |

Create a key for a CI pipeline that deploys one Actor:

```bash [Request]
curl -X POST https://api.scrape.ly/v2/api-keys \
  -H "Authorization: Bearer $SCRAPELY_DEFAULT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "GitHub Actions deploy",
    "description": "Builds janedoe/my-actor on push",
    "expiresAt": "2027-06-30T00:00:00.000Z",
    "fullAccess": false,
    "scopes": ["actors:build", "actors:read"],
    "actorIds": ["janedoe/my-actor"]
  }'
```

```json [Response]
{
  "data": {
    "id": "3f9a0c1d2e4b5a67",
    "name": "GitHub Actions deploy",
    "description": "Builds janedoe/my-actor on push",
    "isDefault": false,
    "prefix": "scrapely_sk_…ab12",
    "fullAccess": false,
    "scopes": ["actors:read", "actors:build"],
    "actorIds": ["eLN5rTB8sYTBWdfzx"],
    "storageIds": [],
    "ipAllowlist": [],
    "runPermission": "limited",
    "maxChargePerRunUsd": null,
    "maxChargePerDayUsd": null,
    "rateLimitPerMin": null,
    "createdAt": "2026-10-17T12:00:00.000Z",
    "updatedAt": "2026-10-17T12:00:00.000Z",
    "lastUsedAt": null,
    "lastUsedIp": null,
    "expiresAt": "2027-06-30T00:00:00.000Z",
    "revokedAt": null,
    "status": "active",
    "key": "scrapely_sk_3f9a0c1d2e4b5a67_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}
```

Revoke it:

```bash
curl -X DELETE -H "Authorization: Bearer $SCRAPELY_DEFAULT_KEY" \
  https://api.scrape.ly/v2/api-keys/3f9a0c1d2e4b5a67
```

## CLI, SDKs, and MCP

**CLI.** `scrapely login` works with any key: the Default key, a full-access key, or a scoped key. It only checks the key with `GET /v2/users/me`. Each command then succeeds or fails according to the key's scopes. See [CLI authentication](https://scrape.ly/docs/cli-auth.md).

```bash
scrapely login --token "$SCRAPELY_TOKEN"
```

**API clients and SDKs.** The [JavaScript](https://scrape.ly/docs/client-js.md) and [Python](https://scrape.ly/docs/client-python.md) clients send the key as a Bearer token, so a scoped key works there the same way.

**MCP.** The [hosted MCP server](https://scrape.ly/docs/mcp.md) acts with the key you give it. A scoped key limits what an AI agent can do: a read-only key can't start paid runs.

## Recommended keys

Use a separate, scoped key for everything that isn't you in the dashboard:

| Use | Scopes |
| --- | --- |
| CI that deploys an Actor (`scrapely push`) | `actors:build` + `actors:read`, restricted to that Actor |
| A backend that triggers runs and reads results | `actors:run` + `runs:read`, restricted to the Actors it uses |
| A dashboard or BI tool that reads results | `datasets:read` (or the **Read only** preset), restricted to the storages it needs |
| An MCP client or AI agent | **Read only**, or `actors:run` + `runs:read` with a daily charge limit |
| A service or agent that sets up automations | The **Automations** preset, without spending caps (a capped key can't create, change, or run automations). Cap each step with **Maximum cost per run** instead |
| A third-party integration | Only the scopes it asks for, with an expiry date |

Give each key a name you'll recognize, set an expiry where you can, and revoke keys you no longer use. Use **Last used** to find keys that have gone quiet.

## What's next

- [API overview](https://scrape.ly/docs/api-overview.md): base URL, errors, and rate limits.
- [Security](https://scrape.ly/docs/security.md): run tokens, secrets, and Actor permissions.
- [CLI authentication](https://scrape.ly/docs/cli-auth.md): logging in and using keys in CI.
- [Account & billing](https://scrape.ly/docs/account.md): plans, usage, and limits.

---

- Previous: [Account & billing](https://scrape.ly/docs/account.md)
- Next: [Notifications](https://scrape.ly/docs/notifications.md)
- All docs: [Docs index](https://scrape.ly/docs.md), [llms.txt](https://scrape.ly/llms.txt)
