Webhooks
HTTP notifications for run and build events, with payloads, retries, and delivery logs.
Webhooks send an HTTP POST to your endpoint when an Actor run or build changes state. All endpoints are prefixed with https://api.scrape.ly/v2. Authenticate with Authorization: Bearer $SCRAPELY_TOKEN.
The /v2/webhooks routes need your personal API token, or an Actor run token with full permissions. Limited-permission run tokens receive 403 insufficient-permissions. The /v2/webhook-dispatches routes accept any token for your account.
To attach a one-off webhook to a single run instead of creating a standing one, pass webhooks when you start the run; see Actors API.
Every webhook except ad-hoc ones is also listed as an automation: GET /v2/automations?include=webhooks and GET /v2/automations/{webhookId} return it with "source": "webhook", and the dashboard shows it under Automations. Nothing on this page changes because of that. See Webhooks as automations.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v2/webhooks | List webhooks you own |
POST | /v2/webhooks | Create a webhook |
GET | /v2/webhooks/{webhookId} | Get a webhook |
PUT | /v2/webhooks/{webhookId} | Update a webhook |
DELETE | /v2/webhooks/{webhookId} | Delete a webhook |
GET | /v2/webhooks/{webhookId}/dispatches | List dispatches for one webhook |
POST | /v2/webhooks/{webhookId}/test | Send a TEST event |
POST | /v2/webhooks/{webhookId}/convert-to-automation | Turn a Trigger Actor or Trigger task webhook into an automation |
GET | /v2/actor-tasks/{taskId}/webhooks | Your webhooks whose condition names this task |
GET | /v2/webhook-dispatches | List dispatches for your account |
GET | /v2/webhook-dispatches/{dispatchId} | Get one dispatch |
Errors and limits
| Status | error.type | When |
|---|---|---|
400 | invalid-input | Missing requestUrl, empty or invalid eventTypes, a malformed URL, a URL that is not allowed (see Allowed URLs), or ACTOR.BUILD.* events with a condition.actorId you don't own. |
403 | insufficient-permissions | A limited-permission Actor run token called a /v2/webhooks route. |
403 | limit-reached | Creating a webhook when you already have your plan's maximum number of webhooks. |
403 | permission-denied | The webhook belongs to another account. |
404 | record-not-found | The webhook or dispatch does not exist, or the dispatch belongs to another account. |
Event types
eventTypes is an array of these values:
| Event | Fires when |
|---|---|
ACTOR.RUN.CREATED | A run is created, however it was started: POST /v2/actors/{actorId}/runs, the synchronous run endpoints, a task run, 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 exceeds its timeout. |
ACTOR.RUN.RESURRECTED | A finished run is resurrected. |
ACTOR.BUILD.CREATED | A build starts. |
ACTOR.BUILD.SUCCEEDED | A build finishes successfully. |
ACTOR.BUILD.FAILED | A build fails. |
ACTOR.BUILD.ABORTED | A build is aborted. |
ACTOR.BUILD.TIMED_OUT | A build exceeds its timeout. |
TEST | Sent by Test webhook. |
Build events are owner only: they fire only for your own Actors, and you can't set them on someone else's Actor.
Conditions
condition narrows which events fire the webhook. Every field you set must match:
| Field | Matches |
|---|---|
actorId | Runs and builds of this Actor. Builds only if you own it. |
actorTaskId | Runs started from this task, including their ACTOR.RUN.CREATED event. ACTOR.RUN.RESURRECTED events do not carry a task ID, so a webhook with this condition does not receive them. |
actorRunId | This run only. Build events never match. |
An empty condition (the default) matches every event of the selected types across your account.
Get list of webhooks
/v2/webhooks| Query param | Type | Description |
|---|---|---|
limit | number | Page size (default 1000, max 1000). |
offset | number | Number of webhooks to skip (default 0). |
desc | boolean | Default is true (newest first). Pass desc=0 for ascending. |
sortBy | string | createdAt (default), description, requestUrl, or isEnabled. |
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
"https://api.scrape.ly/v2/webhooks?limit=20&sortBy=createdAt"lastDispatch and stats are null until the webhook has fired.
/v2/webhooks?limit=5Create webhook
/v2/webhooks| Body field | Type | Required | Description |
|---|---|---|---|
eventTypes | string[] | Yes | Non-empty array of event types. |
requestUrl | string | Yes | HTTP or HTTPS URL that receives the event. See Allowed URLs. |
condition | object | No | actorId, actorTaskId, and/or actorRunId. See Conditions. Defaults to {}. |
payloadTemplate | string | No | Template for the request body. See Payload template. |
headersTemplate | string | No | JSON object, as a string, of extra request headers. See Headers template. |
description | string | No | Human-readable description. |
isAdHoc | boolean | No | Delete the webhook after its first dispatch. |
shouldInterpolateStrings | boolean | No | false leaves placeholders inside JSON strings of payloadTemplate, and in headersTemplate values, as written. Placeholders outside strings are still replaced. |
ignoreSslErrors | boolean | No | Stored and returned for compatibility. TLS certificates are always verified. |
notification | object | No | Send a Slack, Discord, or email message, or write to Google Sheets, instead of a POST. See Notifications. |
doNotRetry | boolean | No | Send each event once, without retries. |
idempotencyKey | string | No | If you already have a webhook with this key, that webhook is returned instead of creating another. |
isEnabled | boolean | No | false creates the webhook turned off (default true). Any other non-boolean value returns 400 invalid-input. |
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"],
"requestUrl": "https://example.com/hooks/scrapely",
"condition": { "actorId": "eLN5rTB8sYTBWdfzx" },
"description": "Notify the data pipeline"
}'Returns 201 with { "data": { ...webhook } }.
Get webhook
/v2/webhooks/{webhookId}curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
https://api.scrape.ly/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4cReturns { "data": { ...webhook } } in the same shape as a list item.
/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4cUpdate webhook
/v2/webhooks/{webhookId}Send only the fields you want to change. Accepts the create fields except idempotencyKey, plus isEnabled. A new requestUrl is checked against Allowed URLs, and eventTypes values must be valid.
| Body field | Type | Description |
|---|---|---|
isEnabled | boolean | Enable or disable the webhook. |
curl -X PUT https://api.scrape.ly/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4c \
-H "Authorization: Bearer $SCRAPELY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"isEnabled": false}'Returns the updated webhook.
Delete webhook
/v2/webhooks/{webhookId}Returns 204 No Content.
curl -X DELETE -H "Authorization: Bearer $SCRAPELY_TOKEN" \
https://api.scrape.ly/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4cTest webhook
/v2/webhooks/{webhookId}/testSends a TEST event with placeholder values (test-actor-id, test-run-id, and so on) to the webhook's requestUrl, using its templates and retry settings, and stores a dispatch record. The test is sent even if the webhook is disabled.
curl -X POST -H "Authorization: Bearer $SCRAPELY_TOKEN" \
https://api.scrape.ly/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4c/testOn failure, success is false and error describes the last attempt.
Delivery
Request
Each event is sent as POST to requestUrl with these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Scrapely-Webhook/1.0 |
X-Scrapely-Webhook-Dispatch-Id | ID of the dispatch record. |
X-Scrapely-Webhook | An HMAC-SHA256 of the body, signed with a platform key. |
Without a payloadTemplate, the body is:
{
"userId": "uSrIdExAmPlE123",
"createdAt": "2026-08-14T15:02:31.000Z",
"eventType": "ACTOR.RUN.SUCCEEDED",
"eventData": {
"actorId": "eLN5rTB8sYTBWdfzx",
"actorTaskId": null,
"actorRunId": "a3f9c1e7b5d20486f",
"userId": "uSrIdExAmPlE123",
"status": "SUCCEEDED",
"startedAt": "2026-08-14T15:00:00.000Z",
"finishedAt": "2026-08-14T15:02:31.000Z",
"durationMillis": 151000,
"defaultDatasetId": "3f6c1a2e-8b4d-4c7a-9e21-5d0b7f3a9c10",
"defaultKeyValueStoreId": "b2d7e4a1-5c3f-4e8a-9f60-1a2b3c4d5e6f",
"defaultRequestQueueId": "7a9e2c4b-1d3f-4b6a-8c5e-0f1a2b3c4d5e",
"buildId": "bUiLdIdExAmPlE123",
"exitCode": 0,
"computeUnits": 0.13
},
"resource": { "...": "same object as eventData" }
}On runs of an Actor you don't own, buildId is left out (and {{buildId}} is an empty string). Dispatch lists leave buildId and buildNumber out of the stored payloads of those runs, also for dispatches sent before builds became owner only.
For build events, eventData has actorId, buildId, userId, status, startedAt, finishedAt, durationMillis, versionNumber, buildTag, and exitCode.
Payload template
payloadTemplate replaces the default body. Placeholders in double braces are replaced with JSON-encoded values, so write them without surrounding quotes:
| Placeholder | Value |
|---|---|
{{userId}}, {{createdAt}}, {{eventType}} | Event fields. |
{{eventData}}, {{resource}} | The whole event data object. |
{{eventData.field}}, {{resource.field}} | One field of the event data, for example {{resource.defaultDatasetId}}. Unknown fields become null. |
{{globals.dateISO}}, {{globals.dateUnix}} | Event time as an ISO string or Unix seconds. |
{{actorId}}, {{actorRunId}}, {{status}}, {{startedAt}}, {{finishedAt}}, {{buildId}}, {{defaultDatasetId}}, {{defaultKeyValueStoreId}}, {{exitCode}}, {{durationMillis}}, {{computeUnits}} | Shortcuts to event data fields, always encoded as strings ({{exitCode}} becomes "0"). Use {{eventData.exitCode}} for a number. |
{ "text": {{status}}, "run": {{actorRunId}}, "dataset": {{resource.defaultDatasetId}}, "sentAt": {{globals.dateISO}} }Inside a JSON string, a placeholder that is the whole string ("{{resource.defaultDatasetId}}") is replaced by its JSON value, and one inside other text ("Run {{actorRunId}}") by its plain text, unless shouldInterpolateStrings is false.
The template is sent as rendered and is not validated, so check that it produces valid JSON.
Headers template
headersTemplate is a JSON object, as a string, whose entries are added to the request headers. These placeholders are replaced with plain text: {{eventType}}, {{actorId}}, {{actorRunId}}, {{status}}, {{userId}}, and {{createdAt}}. Set shouldInterpolateStrings to false to send the values unchanged. A template that is not valid JSON is ignored.
"{\"X-Webhook-Secret\": \"s3cr3t\", \"X-Run-Status\": \"{{status}}\"}"Header values are secrets, so only your Default key, OAuth tokens, and API keys that can write webhooks see them. Read-only and other scoped keys, run tokens, and CI tokens get every value as ********. Sending ******** back in a PUT keeps the stored value. A caller that only sees masked values can't change the requestUrl or condition of a webhook that stores header values unless it also sends a new headersTemplate, and can't send that webhook a test delivery (403 insufficient-permissions).
Retries
A delivery succeeds on any 2xx response. Network errors, timeouts, 429, and 5xx responses are retried up to 5 times, after 1, 2, 4, 8, and 16 seconds. Other 4xx responses fail immediately. Redirects are not followed, so a 3xx response is a failure. Each attempt times out after 30 seconds. Set doNotRetry to send only once.
Allowed URLs
requestUrl can be http or https. URLs that point at localhost, private or internal networks, or Scrape.ly infrastructure are rejected, both when you save the webhook and again when it is sent. The one exception is the Scrape.ly API's run endpoints, /v2/actors/{actorId}/runs and /v2/actor-tasks/{taskId}/runs, so one event can start another run. Those endpoints need a token, so send it from headersTemplate:
{
"eventTypes": ["ACTOR.RUN.SUCCEEDED"],
"condition": { "actorId": "eLN5rTB8sYTBWdfzx" },
"requestUrl": "https://api.scrape.ly/v2/actors/kT2pQ9mWx4LbN7sVc/runs",
"headersTemplate": "{\"Authorization\": \"Bearer <your token>\"}",
"payloadTemplate": "{\"datasetId\": {{resource.defaultDatasetId}}}"
}The rendered payload becomes the second run's input.
Notifications
A webhook with a notification object sends a ready-made message, or writes the run's results to a Google Sheet, instead of a POST. Leave out requestUrl, payloadTemplate, and headersTemplate. Only the run-finished events are allowed (ACTOR.RUN.SUCCEEDED, FAILED, ABORTED, TIMED_OUT); Google Sheets takes only ACTOR.RUN.SUCCEEDED. These webhooks need your personal API token and can't be ad hoc.
notification field | Channels | Description |
|---|---|---|
channel | all | slack, discord, email, or gsheets. Can't be changed later. |
discordWebhookUrl | discord | A https://discord.com/api/webhooks/… URL. Stored encrypted; responses show it masked. |
email | email | Recipient (required). Your account email works right away; any other address gets a confirmation link first. |
onlyWithResults | slack, discord, email | Skip succeeded runs that returned no items. Default false. |
previewItems | slack, discord, email | First 0 to 5 items shown in the message. Default 3. |
message | slack, discord, email | Your own text above the run details, up to 1,000 characters. Variables: {{actorName}}, {{taskName}}, {{status}}, {{runId}}, {{duration}}, {{itemCount}}, {{cost}}, {{runUrl}}, {{datasetUrl}}. |
sheetUrl | gsheets | Link to a sheet shared with the address from GET /v2/integrations/gsheets. |
sheetName | gsheets | Tab to write to; null for the first tab. |
mode | gsheets | append (add rows) or replace (clear the tab first). |
maxRows | gsheets | Items written per run, 1 to 10,000. Default 1000. |
Slack posts to the channel connected under Settings → Connections. Responses carry notification with channel, a masked target, and the options; for Slack also connected, for email emailState. POST /v2/webhooks/{webhookId}/test sends a test message. Limits, retries, and message contents are described in Slack, Discord and Email and Google Sheets.
curl -X POST https://api.scrape.ly/v2/webhooks \
-H "Authorization: Bearer $SCRAPELY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"eventTypes": ["ACTOR.RUN.FAILED", "ACTOR.RUN.TIMED_OUT"],
"condition": { "actorId": "eLN5rTB8sYTBWdfzx" },
"description": "Slack when the crawl fails",
"notification": { "channel": "slack", "previewItems": 0 }
}'List webhooks of a task
/v2/actor-tasks/{taskId}/webhooksYour webhooks whose condition.actorTaskId names this task, in the same shape as Get list of webhooks. {taskId} can be the task ID or username~task-name. Paginated with limit (default and max 1000), offset, and desc (default false). The task must be yours: another account's task returns 403 forbidden, an unknown one 404 record-or-token-not-found. On a scoped key it needs webhooks:read. For an Actor's webhooks, see List Actor webhooks.
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
"https://api.scrape.ly/v2/actor-tasks/kQ7mN2pXv9LsT4wRb/webhooks"/v2/actor-tasks/{taskId}/webhooksList dispatches for a webhook
/v2/webhooks/{webhookId}/dispatchesLists dispatch records for one webhook, newest first. There is one record per event, not per retry attempt. A webhook you don't own returns an empty list.
| Query param | Type | Description |
|---|---|---|
limit | number | Page size (default 1000, max 1000). |
offset | number | Number of records to skip (default 0). |
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
"https://api.scrape.ly/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4c/dispatches?limit=20"statusissucceededorfailed(lower case), after all retries.requestPayloadholds the event (createdAt,eventType,eventData), not the renderedpayloadTemplate.- Records are kept for 2 days on the free plan, 31, 60 or 90 days on the Starter, Scale or Business plan, and at most the newest 500 per webhook.
/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4c/dispatches?limit=20List webhook dispatches
/v2/webhook-dispatchesLists dispatch records across all your webhooks, newest first. Automation webhook deliveries are listed too, with the automation's ID as webhookId and the event type AUTOMATION.RUN.SUCCEEDED or AUTOMATION.RUN.FAILED.
| Query param | Type | Description |
|---|---|---|
limit | number | Page size (default 1000, max 1000). |
offset | number | Number of records to skip (default 0). |
runId | string | Only dispatches for this Actor run. |
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
"https://api.scrape.ly/v2/webhook-dispatches?limit=20"The response matches List dispatches for a webhook.
/v2/webhook-dispatches?limit=5Get webhook dispatch
/v2/webhook-dispatches/{dispatchId}curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
https://api.scrape.ly/v2/webhook-dispatches/c8f2a6d4-3b1e-4f7a-a9c5-6e0d2b4f8a1cReturns { "data": { ...dispatch } } with the same fields as a list item.
/v2/webhook-dispatches/c8f2a6d4-3b1e-4f7a-a9c5-6e0d2b4f8a1c