---
title: "Tasks, schedules & webhooks · Scrape.ly Docs"
description: "Saved inputs, cron schedules, webhook and message reference, CI rebuilds, and connected accounts."
url: https://scrape.ly/docs/integrations
markdown: https://scrape.ly/docs/integrations.md
---

# Tasks, schedules & webhooks

> Saved inputs, cron schedules, webhook and message reference, CI rebuilds, and connected accounts.

Once an Actor does what you need, automate it. **Saved inputs** (tasks in the API) keep an input and run options under a name. **[Automations](https://scrape.ly/docs/automations.md)** do everything else: run Actors on a timetable, message you on Slack, Discord, or email when a run finishes, write results to Google Sheets, call your systems with a webhook, or start another Actor after a run. If you build Actors, **CI** rebuilds them on every git push. Most of this works from the dashboard and the [API](https://scrape.ly/docs/api-overview.md).

In the dashboard there is one place for all of it: **Automations** in the side menu, and the **Automations** tab of an Actor or a saved input. A message or webhook about a run is an automation with two parts: **when** (a run of an Actor finishes, with the outcomes you pick) and **then** (one place to send it). This page is the reference for what those messages and webhooks contain; [Automations](https://scrape.ly/docs/automations.md) explains the editor.

## Tasks

A task is a saved configuration of one Actor: an input plus run options (build, memory, timeout, maximum cost). The dashboard calls tasks **saved inputs**; see [Saved inputs](https://scrape.ly/docs/run-actors.md#saved-inputs). The build option only applies to your own Actors; someone else's Actor runs its [User Build](https://scrape.ly/docs/publishing.md#user-build). Tasks are useful for a crawl you repeat with the same settings, for scheduling, and for starting a known configuration from a webhook.

### Create a task

- **From an Actor**: set up the input on the **Input** tab, then choose **Save as new…** in the **Input** dropdown. The saved input keeps the current input and run options.
- **From the list**: open **Actors** → **Saved inputs** in the side menu (or **Manage all saved inputs** in an Actor's **Input** dropdown) and click **Create**.
- **From the API**: `POST /v2/actor-tasks` with `actId` (the Actor's ID), `name`, and optionally `title`, `input`, and `options` (`build`, `timeoutSecs`, `memoryMbytes`, `maxItems`, `maxTotalChargeUsd`, `restartOnError`). `build` only applies to your own Actors; someone else's Actor always runs its [User Build](https://scrape.ly/docs/publishing.md#user-build).

```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-coffee-shops",
    "input": {"searchTerms": ["coffee"], "maxResults": 100},
    "options": {"memoryMbytes": 1024, "timeoutSecs": 900}
  }'
```

Task names can be up to 30 characters and must be unique in your account (`409` otherwise). An empty `input` (`{}`) is filled with the defaults from the Actor's input schema. Your plan limits how many tasks you can have (`403` when you reach it).

### Run a task

Open the task and click **Start**. From the API:

```
POST /v2/actor-tasks/{taskId}/runs
POST /v2/actor-tasks/{taskId}/run-sync
POST /v2/actor-tasks/{taskId}/run-sync-get-dataset-items
GET  /v2/actor-tasks/{taskId}/runs/last
```

The request body is optional. Any fields you send are merged over the task's saved input, one level deep, so you can override a single field for one run. Query parameters such as `memory`, `timeout`, `build` (your own Actors only), `maxTotalChargeUsd`, and `waitForFinish` work as they do for [Actor runs](https://scrape.ly/docs/run-actors.md#query-parameters) and override the task's options. Only the task's owner can run it.

From the CLI, `scrapely task run <taskId>` accepts `--build` (your own Actors only), `--memory`, `--timeout`, and `--json`. The CLI can also publish and unpublish tasks, but you create and edit them in the dashboard or through the API.

See the [Tasks API](https://scrape.ly/docs/api-tasks.md).

## Schedules

A schedule starts one or more Actors or saved inputs on a cron timetable. In the dashboard, a schedule is an [automation](https://scrape.ly/docs/automations.md) with **On a schedule** as its trigger: open **Automations** in the side menu and click **New automation**. The editor shows the input of each run, lets you change single fields for the automation (**Customize**), and starts the time zone at your browser's.

The cron expression is evaluated in the schedule's time zone, including daylight saving. For example, `0 9 * * *` with `Europe/Berlin` fires at 09:00 Berlin time all year.

### Create a schedule with the API

```bash
curl -X POST https://api.scrape.ly/v2/schedules \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "weekday-mornings",
    "cronExpression": "0 7 * * 1-5",
    "isEnabled": true,
    "actions": [
      {
        "type": "RUN_ACTOR_TASK",
        "actorTaskId": "<taskId>"
      },
      {
        "type": "RUN_ACTOR",
        "actorId": "<actorId>",
        "runInput": {"body": "{\"maxResults\": 20}", "contentType": "application/json"},
        "runOptions": {"memoryMbytes": 1024, "timeoutSecs": 600}
      }
    ]
  }'
```

- `name`, `cronExpression`, and at least one action are required. A schedule can hold up to 10 Actor actions and 10 task actions.
- `runInput.body` is the input as a JSON string; set `contentType` to `application/json` so it is parsed. On a `RUN_ACTOR` action, input is chosen in this order: the action's `runInput`, then your default saved input, then the schema defaults. On a `RUN_ACTOR_TASK` action, the `runInput` fields are merged over the task's input, one level deep.
- `runOptions` accepts `build` (your own Actors only; someone else's Actor always runs its [User Build](https://scrape.ly/docs/publishing.md#user-build)), `timeoutSecs`, `memoryMbytes`, `maxItems`, `maxTotalChargeUsd`, and `restartOnError`. Missing options come from the task, then from the Actor's defaults.
- `isExclusive: true` skips a firing while the previous firing's runs are still going.
- `notificationsEnabled: false` turns off the schedule's failure alert.

Scheduled runs have the origin `SCHEDULER` and are billed like any other run. A Full Access Actor must already be approved, or its scheduled runs fail to start. `GET /v2/schedules/{scheduleId}/log` lists recent firings. To test a schedule, use **Run now** on the automation, or `POST /v2/automations/{id}/runs` with the schedule's ID. See the [Schedules API](https://scrape.ly/docs/api-schedules.md) and the [Automations API](https://scrape.ly/docs/api-automations.md), which also has filters, an AI step, and deliveries.

### Cron syntax

A cron expression has five fields: minute (0–59), hour (0–23), day of month (1–31), month (1–12 or `JAN`–`DEC`), and day of week (0–7 or `SUN`–`SAT`, where 0 and 7 are Sunday).

| Form | Example | Meaning |
| --- | --- | --- |
| `*` | `* * * * *` | Every minute |
| Number | `30 6 * * *` | 06:30 every day |
| Range | `0 9 * * MON-FRI` | 09:00 Monday to Friday |
| List | `0 8,12,18 * * *` | 08:00, 12:00, and 18:00 |
| Step | `*/15 * * * *` | Every 15 minutes |
| Stepped range | `0 8-18/2 * * *` | Every 2 hours from 08:00 to 18:00 |
| Macro | `@daily` | Midnight every day (also `@hourly`, `@weekly`, `@monthly`, `@yearly`, `@annually`, `@midnight`) |

If you restrict both day of month and day of week, a day matches if either matches, as in standard cron. For daylight-saving details, see [Cron expressions](https://scrape.ly/docs/api-schedules.md#cron-expressions).

## Webhooks

A webhook sends an HTTP POST to a URL when an event happens, for example when a run succeeds. Use webhooks to push results into your pipeline instead of polling, or to start another Actor or task when a run finishes.

### Set up a webhook in the dashboard

A webhook is an automation: open **Automations** (or an Actor's **Automations** tab) and click **New automation**, then pick **Call a webhook when a run finishes**. The automation starts out as "when a run of this Actor finishes, send a POST to this URL". You can change:

- **When**: the Actor, or one of its saved inputs, or every run in your account, and the events (see [Event types](#event-types)).
- **Then**: the **URL**, an optional payload template and headers template, and the options **Ignore SSL errors**, **Interpolate variables in string fields**, and **Do not retry on failure**.

Such an automation is stored as a webhook, so it is also listed by `GET /v2/webhooks`. The other way round, every webhook you create through the API, an SDK, or an API client is listed under **Automations** too. There you can turn it on or off, edit it, send a test, and open its delivery history.

To start another Actor or saved input after a run, use the **Start another actor after this one** recipe. It creates an automation that runs after another run finishes, which can also wait for several runs, filter, summarize, and send. Its step input can take values from the finished run, such as `{{resource.defaultDatasetId}}`; see [Automations](https://scrape.ly/docs/automations.md#input-from-the-finished-run).

> [!NOTE]
> Older **Trigger actor** and **Trigger saved input** webhooks (they call `/v2/actors/{actorId}/runs` or `/v2/actor-tasks/{taskId}/runs` on the Scrape.ly API) keep working and are listed under **Automations**. They carry an integration token (`sit_…`) in their headers template, not your API key. The token is bound to that one webhook and can only start the Actor or task it names. Rotating your API key doesn't affect it; deleting the webhook revokes it. If you put your account API key in any webhook's headers yourself, Scrape.ly replaces it with an integration token when you save. Such a webhook can be turned into a regular automation; see [Convert a webhook](https://scrape.ly/docs/automations.md#convert-a-webhook-to-an-automation).

### Create a webhook with the API

```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", "ACTOR.RUN.FAILED"],
    "condition": {"actorId": "<actorId>"},
    "requestUrl": "https://example.com/hooks/scrapely",
    "description": "Notify pipeline"
  }'
```

- `eventTypes` and `requestUrl` are required.
- `condition` limits the webhook to one Actor (`actorId`), one task (`actorTaskId`), or one run (`actorRunId`). **Without a condition, the webhook fires for every matching event in your account.**
- Optional fields: `payloadTemplate`, `headersTemplate`, `description`, `ignoreSslErrors`, `doNotRetry`, `shouldInterpolateStrings`, `isEnabled` (`false` creates it turned off), and `idempotencyKey` (repeating a create with the same key returns the existing webhook).
- The URL must be reachable from the internet. Private, local, and internal-network addresses are rejected. The Scrape.ly API itself is allowed, so a webhook can start another run.
- Your plan limits the number of webhooks (100 by default).

See the [Webhooks API](https://scrape.ly/docs/api-webhooks.md).

### Event types

| Event | Fires when |
| --- | --- |
| `ACTOR.RUN.CREATED` | A run starts, however it was started: the dashboard, the API (including synchronous runs), a task, a schedule, or an automation |
| `ACTOR.RUN.SUCCEEDED` | A run finishes successfully |
| `ACTOR.RUN.FAILED` | A run fails |
| `ACTOR.RUN.ABORTED` | A run is aborted |
| `ACTOR.RUN.TIMED_OUT` | A run reaches its timeout |
| `ACTOR.RUN.RESURRECTED` | A finished run is resurrected |
| `ACTOR.BUILD.CREATED` | A build of one of your Actors starts |
| `ACTOR.BUILD.SUCCEEDED` | A build succeeds |
| `ACTOR.BUILD.FAILED` | A build fails |
| `ACTOR.BUILD.ABORTED` | A build is aborted |
| `ACTOR.BUILD.TIMED_OUT` | A build reaches its timeout |
| `TEST` | You send a test with `POST /v2/webhooks/{webhookId}/test` |

Run events fire for your own runs, including runs of other people's Actors. Build events fire only for Actors you own, and setting them on someone else's Actor is rejected with `400 invalid-input`.

### Payload

By default the request body is:

```json
{
  "userId": "<your user ID>",
  "createdAt": "2026-10-12T09:00:00.000Z",
  "eventType": "ACTOR.RUN.SUCCEEDED",
  "eventData": {
    "actorId": "...",
    "actorTaskId": null,
    "actorRunId": "...",
    "status": "SUCCEEDED",
    "startedAt": "...",
    "finishedAt": "...",
    "durationMillis": 41234,
    "defaultDatasetId": "...",
    "defaultKeyValueStoreId": "...",
    "defaultRequestQueueId": "...",
    "buildId": "...",
    "exitCode": 0,
    "computeUnits": 0.012
  },
  "resource": { "...": "same fields as eventData" }
}
```

On runs of other people's Actors, `buildId` is left out: build info is owner only.

For build events, `eventData` has `actorId`, `buildId`, `status`, `versionNumber`, `buildTag`, `exitCode`, and timing fields.

### Payload and header templates

A `payloadTemplate` replaces the default body. These variables are available:

- `{{userId}}`, `{{createdAt}}`, `{{eventType}}`, `{{eventData}}`, `{{resource}}`, and `{{globals}}` (with `dateISO` and `dateUnix`).
- Dotted paths of any depth: `{{eventData.actorRunId}}`, `{{resource.defaultDatasetId}}`, `{{resource.stats.computeUnits}}`, and so on.
- Shortcuts: `{{actorId}}`, `{{actorRunId}}`, `{{status}}`, `{{startedAt}}`, `{{finishedAt}}`, `{{buildId}}`, `{{defaultDatasetId}}`, `{{defaultKeyValueStoreId}}`, `{{exitCode}}`, `{{durationMillis}}`, `{{computeUnits}}`.

Outside quotation marks, each variable is replaced by its JSON value, and strings include their quotes:

```json
{
  "text": "Run finished",
  "runId": {{actorRunId}},
  "status": {{status}},
  "datasetId": {{resource.defaultDatasetId}}
}
```

Inside a JSON string, a variable that is the whole string (`"{{resource.defaultDatasetId}}"`) is replaced by its JSON value, and a variable inside other text (`"Run {{actorRunId}} done"`) by its plain text. `shouldInterpolateStrings: false` leaves strings as written.

A `headersTemplate` is a JSON object of extra request headers. Inside header values you can use `{{eventType}}`, `{{actorId}}`, `{{actorRunId}}`, `{{status}}`, `{{userId}}`, and `{{createdAt}}`, inserted as plain text. Setting `shouldInterpolateStrings` to `false` turns off variables in headers.

### Delivery, retries, and verification

Every request carries `Content-Type: application/json`, `User-Agent: Scrapely-Webhook/1.0`, and an `X-Scrapely-Webhook-Dispatch-Id` header that stays the same across retries of one delivery, so you can use it to ignore duplicates.

- Your endpoint has 30 seconds to respond. Redirects are not followed.
- Network errors, timeouts, `5xx`, and `429` responses are retried up to 5 times, after 1, 2, 4, 8, and 16 seconds. Other `4xx` responses are not retried. `doNotRetry: true` turns retries off.
- To check that a request really comes from your webhook, put a secret header in the `headersTemplate` (for example `{"X-Webhook-Token": "<long random string>"}`) and verify it on your server.

Each delivery is recorded as a dispatch with its status code and response. Up to 500 recent dispatches are kept per webhook.

```
GET  /v2/webhooks/{webhookId}/dispatches
GET  /v2/webhook-dispatches
POST /v2/webhooks/{webhookId}/test
```

### Ad-hoc webhooks

An ad-hoc webhook is created for a single use and deleted after it fires once.

When you start a run through the API, pass ad-hoc webhooks in the `webhooks` query parameter as a base64-encoded JSON array:

```bash
WEBHOOKS=$(echo -n '[{"eventTypes":["ACTOR.RUN.SUCCEEDED"],"requestUrl":"https://example.com/hook"}]' | base64)

curl -X POST "https://api.scrape.ly/v2/actors/<actorId>/runs?webhooks=$WEBHOOKS" \
  -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

You can also send a `webhooks` array in the JSON body. Each ad-hoc webhook is bound to the run it was sent with, so it never fires for another run of the same Actor or task. While its run is active, it counts toward your plan's webhook limit; past the limit the run start returns `403 limit-reached` and no run is created. A run token of a limited-permission Actor can't attach ad-hoc webhooks (`403 insufficient-permissions`).

From inside an Actor, `Actor.addWebhook()` (JavaScript) or `Actor.add_webhook()` (Python) registers an ad-hoc webhook for the current run. See [JavaScript SDK](https://scrape.ly/docs/sdk-node-actors.md) and [Python SDK](https://scrape.ly/docs/sdk-python-actors.md).

## CI/CD: rebuild on git push

For Actors whose source is a Git repository, Scrape.ly can start a new `latest` build on every push.

If you've connected GitHub (see below), this is set up for you. When you link a version to a GitHub repository, Scrape.ly adds a push webhook to that repository. This happens whether you create the Actor, change its versions, or point a version at a new repository, in the dashboard or through the API with your API key. If an older webhook for the same Actor and version is already there, for example one that carries your API key instead of the build secret, Scrape.ly updates it instead of adding a second one. Linking never fails because of the webhook; if GitHub refuses it, add it by hand as described below.

A push only starts a build when it comes from the linked repository and updates the linked branch: the `#branch` in the repository URL, or the repository's default branch if the URL has none. A tag with that name also counts. Other pushes are answered with `200` and no build starts, so pushes to feature branches don't spend build minutes. See [Start a build](https://scrape.ly/docs/api-actors.md#start-a-build) for the response.

To set it up by hand, open the Actor, go to the **Settings** tab, and find **Continuous integration (GitHub)**, or fetch the build URL from the API:

```bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actors/<actorId>/ci/webhook-url"
```

The returned `webhookUrl` contains a build secret for that Actor only, not your API token. Pass `version=<versionNumber>` to target a specific version; by default it uses the Actor's Git-repository version. Then either:

- **GitHub webhook**: in the repository, open **Settings** → **Webhooks** → **Add webhook**, paste the URL as the **Payload URL**, set the content type to `application/json`, and select the push event.
- **GitHub Actions**: store the URL as a repository secret named `BUILD_ACTOR_URL` and add a workflow step:

```yaml
- name: Trigger Scrapely build
  run: curl -fsS -X POST "$BUILD_ACTOR_URL"
  env:
    BUILD_ACTOR_URL: ${{ secrets.BUILD_ACTOR_URL }}
```

Each request pulls the latest code from the linked repository and builds it. The GitHub Actions call has no `X-GitHub-Event` header, so the branch filter doesn't apply; choose the branches in the workflow's `on:` trigger.

To add or repair the webhook for an Actor you linked before automatic registration, call `POST /v2/actors/{actorId}/ci/webhook` with an empty body. It registers the webhook on the linked repository, or fixes the existing one. See [CI webhooks](https://scrape.ly/docs/api-actors.md#ci-webhooks). `scrapely create --source github --auto-build on` also registers it for new projects. See [CLI CI/CD](https://scrape.ly/docs/cli-config.md) for running CLI commands in a pipeline.

## GitHub and Slack accounts

Connect accounts under **Settings** → **Connections**:

- **GitHub** lets you create Actors from your repositories, and registers a push webhook on each repository you link so pushes rebuild the Actor. `GET /v2/integrations/github/repos` lists the repositories Scrape.ly can see.
- **Slack** connects a workspace and one channel. [Slack messages](#slack-discord-and-email) from your automations go there. You can also point an HTTP webhook at a Slack incoming-webhook URL with your own payload template.

```
GET    /v2/integrations
DELETE /v2/integrations/{type}
```

`GET /v2/integrations` returns the connection status of `slack` and `github`. `DELETE` disconnects one. See [Search, alerts & utilities](https://scrape.ly/docs/api-misc.md#integrations).

## Slack, Discord and Email

An automation that reacts to a run and sends to Slack, Discord, or email posts a ready-made message about that run. Create one with **New automation** and the recipe **Notify me on Slack when a run fails** or **Email me the results**, then change the channel or the outcomes if you like. For one message about several runs, with filters or an AI summary, add run steps to the automation instead. Each one has:

- **Send a message when a run**: succeeded, failed, timed out, or was aborted.
- **Only for runs with results**: skips successful runs that returned no items. Failed runs are always sent.
- **Results preview**: the first 0 to 5 dataset items, shown as JSON.
- **Custom message** (optional): shown above the run details. Variables: `{{actorName}}`, `{{taskName}}`, `{{status}}`, `{{runId}}`, `{{itemCount}}`, `{{duration}}`, `{{cost}}`, `{{runUrl}}`, `{{datasetUrl}}`.

Every message shows the Actor, status, duration, result count and cost, with links to the run and its dataset in the dashboard. Send a test from the automation's page to check the setup with sample data. The automation's page also shows the delivery history, with the reason for any failed delivery.

**Where messages go**

- **Slack**: the channel you picked when you connected Slack under **Settings** → **Connections**. **Change channel** reconnects Slack; the new channel applies to all your Slack automations.
- **Discord**: a channel webhook URL. In Discord, open the channel's settings → **Integrations** → **Webhooks** → **New Webhook** → **Copy Webhook URL**. Only `https://discord.com/api/webhooks/…` URLs are accepted. The URL is stored encrypted and shown masked afterwards. Messages never ping `@everyone` or users.
- **Email**: your account email works right away. Any other address gets a confirmation link first; nothing is sent to it until the link is opened. Every email has an unsubscribe link, and turning off email notifications under **Settings** → **Notifications** stops them too.

**Limits**: each Slack or Discord automation of this kind sends at most 20 messages a minute; each email one at most 30 emails an hour, and an account at most 100 of these emails a day. Messages over a limit are logged as failed deliveries. Slack and Discord deliveries are retried on rate limits, server errors and timeouts.

**API**: these automations are webhooks with a `notification` object instead of a `requestUrl`, and are listed under `/v2/webhooks` and `/v2/automations?include=webhooks`:

```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", "ACTOR.RUN.FAILED"],
    "condition": { "actorId": "YOUR_ACTOR_ID" },
    "description": "Discord alerts",
    "notification": {
      "channel": "discord",
      "discordWebhookUrl": "https://discord.com/api/webhooks/…",
      "onlyWithResults": true,
      "previewItems": 3,
      "message": "{{actorName}}: {{itemCount}} results"
    }
  }'
```

`channel` is `slack`, `discord`, or `email` (with `email`). Responses carry `notification` with a masked `target`; the Discord URL is never returned. `POST /v2/webhooks/{id}/test` sends a test message. Only the run-finished events are allowed (`ACTOR.RUN.SUCCEEDED`, `FAILED`, `ABORTED`, `TIMED_OUT`). `POST /v2/integrations/notifications/email/verify` with `{ "email": "…" }` sends a confirmation link, and `GET /v2/integrations/notifications/email` lists your addresses and their state.

## Google Sheets

Writes the dataset items of each successful run into a Google Sheet. There is no Google sign-in: Scrape.ly writes as its own service account, and you give it access to just the sheets you choose.

1. Click **New automation** and pick **Send results to Google Sheets** (on an Actor's **Automations** tab the Actor is already chosen).
2. Copy the email shown under **Share your sheet with**. In Google Sheets, click **Share**, add that email and give it **Editor** access.
3. Paste the sheet's link, and optionally the **Tab** to write to (the first tab if empty).
4. Pick what happens **On each run**:
   - **Add rows**: new results go below the existing rows.
   - **Replace the tab**: the tab is cleared and filled with the latest results.
5. Save and send a test. A `Scrapely connected ✓` row appears in the sheet; the first real run puts its header in that row.

**Columns**: the first row is a header. Nested fields become columns named with dots, such as `price.amount`; arrays are written as JSON text. With **Add rows**, columns already in the header keep their position and new fields are added at the end. Values are written as plain text and numbers, so text that starts with `=` is never run as a formula. A cell holds at most 50,000 characters.

**When it writes**: only for succeeded runs, and only when the run returned items. **Max rows per run** (1 to 10,000, default 1,000) caps how many items are written.

**Limits**: each Google Sheets automation of this kind writes at most 30 times an hour, and an account at most 500 times a day. If the sheet isn't shared with the service account, or the tab doesn't exist, the delivery history on the automation's page says so.

**API**: a webhook with a `gsheets` notification. Only `ACTOR.RUN.SUCCEEDED` is allowed.

```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"],
    "condition": { "actorId": "YOUR_ACTOR_ID" },
    "description": "Results to Google Sheets",
    "notification": {
      "channel": "gsheets",
      "sheetUrl": "https://docs.google.com/spreadsheets/d/…/edit",
      "sheetName": "Results",
      "mode": "append",
      "maxRows": 1000
    }
  }'
```

`mode` is `append` or `replace`, and `sheetName` may be `null` for the first tab. `GET /v2/integrations/gsheets` returns `serviceAccountEmail`, the address to share sheets with.

## Notifications

Email and dashboard alerts for failed runs, storage expiry, low balance, and more are configured under **Settings** → **Notifications**. See [Notifications](https://scrape.ly/docs/notifications.md).

## What's next

- [Run Actors](https://scrape.ly/docs/run-actors.md): run options and limits
- [Results & exports](https://scrape.ly/docs/results.md): what to fetch when a webhook fires
- [Automations](https://scrape.ly/docs/automations.md): schedules, messages, webhooks, filters, an AI step, and deliveries in one place
- [Automations API](https://scrape.ly/docs/api-automations.md), [Webhooks API](https://scrape.ly/docs/api-webhooks.md), [Schedules API](https://scrape.ly/docs/api-schedules.md), and [Tasks API](https://scrape.ly/docs/api-tasks.md)

---

- Previous: [Automations](https://scrape.ly/docs/automations.md)
- Next: [JavaScript client](https://scrape.ly/docs/client-js.md)
- All docs: [Docs index](https://scrape.ly/docs.md), [llms.txt](https://scrape.ly/llms.txt)
