Skip to content
API

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

MethodPathDescription
GET/v2/webhooksList webhooks you own
POST/v2/webhooksCreate 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}/dispatchesList dispatches for one webhook
POST/v2/webhooks/{webhookId}/testSend a TEST event
POST/v2/webhooks/{webhookId}/convert-to-automationTurn a Trigger Actor or Trigger task webhook into an automation
GET/v2/actor-tasks/{taskId}/webhooksYour webhooks whose condition names this task
GET/v2/webhook-dispatchesList dispatches for your account
GET/v2/webhook-dispatches/{dispatchId}Get one dispatch

Errors and limits

Statuserror.typeWhen
400invalid-inputMissing 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.
403insufficient-permissionsA limited-permission Actor run token called a /v2/webhooks route.
403limit-reachedCreating a webhook when you already have your plan's maximum number of webhooks.
403permission-deniedThe webhook belongs to another account.
404record-not-foundThe webhook or dispatch does not exist, or the dispatch belongs to another account.

Event types

eventTypes is an array of these values:

EventFires when
ACTOR.RUN.CREATEDA 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.SUCCEEDEDA run finishes successfully.
ACTOR.RUN.FAILEDA run fails.
ACTOR.RUN.ABORTEDA run is aborted.
ACTOR.RUN.TIMED_OUTA run exceeds its timeout.
ACTOR.RUN.RESURRECTEDA finished run is resurrected.
ACTOR.BUILD.CREATEDA build starts.
ACTOR.BUILD.SUCCEEDEDA build finishes successfully.
ACTOR.BUILD.FAILEDA build fails.
ACTOR.BUILD.ABORTEDA build is aborted.
ACTOR.BUILD.TIMED_OUTA build exceeds its timeout.
TESTSent 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:

FieldMatches
actorIdRuns and builds of this Actor. Builds only if you own it.
actorTaskIdRuns 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.
actorRunIdThis 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

GET/v2/webhooks
Query paramTypeDescription
limitnumberPage size (default 1000, max 1000).
offsetnumberNumber of webhooks to skip (default 0).
descbooleanDefault is true (newest first). Pass desc=0 for ascending.
sortBystringcreatedAt (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.

GET/v2/webhooks?limit=5

Create webhook

POST/v2/webhooks
Body fieldTypeRequiredDescription
eventTypesstring[]YesNon-empty array of event types.
requestUrlstringYesHTTP or HTTPS URL that receives the event. See Allowed URLs.
conditionobjectNoactorId, actorTaskId, and/or actorRunId. See Conditions. Defaults to {}.
payloadTemplatestringNoTemplate for the request body. See Payload template.
headersTemplatestringNoJSON object, as a string, of extra request headers. See Headers template.
descriptionstringNoHuman-readable description.
isAdHocbooleanNoDelete the webhook after its first dispatch.
shouldInterpolateStringsbooleanNofalse leaves placeholders inside JSON strings of payloadTemplate, and in headersTemplate values, as written. Placeholders outside strings are still replaced.
ignoreSslErrorsbooleanNoStored and returned for compatibility. TLS certificates are always verified.
notificationobjectNoSend a Slack, Discord, or email message, or write to Google Sheets, instead of a POST. See Notifications.
doNotRetrybooleanNoSend each event once, without retries.
idempotencyKeystringNoIf you already have a webhook with this key, that webhook is returned instead of creating another.
isEnabledbooleanNofalse creates the webhook turned off (default true). Any other non-boolean value returns 400 invalid-input.
.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"],
    "requestUrl": "https://example.com/hooks/scrapely",
    "condition": { "actorId": "eLN5rTB8sYTBWdfzx" },
    "description": "Notify the data pipeline"
  }'

Returns 201 with { "data": { ...webhook } }.

Get webhook

GET/v2/webhooks/{webhookId}
.bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4c

Returns { "data": { ...webhook } } in the same shape as a list item.

GET/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4c

Update webhook

PUT/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 fieldTypeDescription
isEnabledbooleanEnable or disable the webhook.
.bash
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

DELETE/v2/webhooks/{webhookId}

Returns 204 No Content.

.bash
curl -X DELETE -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4c

Test webhook

POST/v2/webhooks/{webhookId}/test

Sends 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/test

On failure, success is false and error describes the last attempt.

Delivery

Request

Each event is sent as POST to requestUrl with these headers:

HeaderValue
Content-Typeapplication/json
User-AgentScrapely-Webhook/1.0
X-Scrapely-Webhook-Dispatch-IdID of the dispatch record.
X-Scrapely-WebhookAn HMAC-SHA256 of the body, signed with a platform key.

Without a payloadTemplate, the body is:

.json
{
  "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:

PlaceholderValue
{{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.
.json
{ "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.

.json
"{\"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:

.json
{
  "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 fieldChannelsDescription
channelallslack, discord, email, or gsheets. Can't be changed later.
discordWebhookUrldiscordA https://discord.com/api/webhooks/… URL. Stored encrypted; responses show it masked.
emailemailRecipient (required). Your account email works right away; any other address gets a confirmation link first.
onlyWithResultsslack, discord, emailSkip succeeded runs that returned no items. Default false.
previewItemsslack, discord, emailFirst 0 to 5 items shown in the message. Default 3.
messageslack, discord, emailYour own text above the run details, up to 1,000 characters. Variables: {{actorName}}, {{taskName}}, {{status}}, {{runId}}, {{duration}}, {{itemCount}}, {{cost}}, {{runUrl}}, {{datasetUrl}}.
sheetUrlgsheetsLink to a sheet shared with the address from GET /v2/integrations/gsheets.
sheetNamegsheetsTab to write to; null for the first tab.
modegsheetsappend (add rows) or replace (clear the tab first).
maxRowsgsheetsItems 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.

.bash
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

GET/v2/actor-tasks/{taskId}/webhooks

Your 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.

.bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/actor-tasks/kQ7mN2pXv9LsT4wRb/webhooks"
GET/v2/actor-tasks/{taskId}/webhooks

List dispatches for a webhook

GET/v2/webhooks/{webhookId}/dispatches

Lists 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 paramTypeDescription
limitnumberPage size (default 1000, max 1000).
offsetnumberNumber 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"
  • status is succeeded or failed (lower case), after all retries.
  • requestPayload holds the event (createdAt, eventType, eventData), not the rendered payloadTemplate.
  • 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.
GET/v2/webhooks/5e1c7b9a-2f4d-4a8e-b3c6-9d0e1f2a3b4c/dispatches?limit=20

List webhook dispatches

GET/v2/webhook-dispatches

Lists 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 paramTypeDescription
limitnumberPage size (default 1000, max 1000).
offsetnumberNumber of records to skip (default 0).
runIdstringOnly dispatches for this Actor run.
.bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  "https://api.scrape.ly/v2/webhook-dispatches?limit=20"

The response matches List dispatches for a webhook.

GET/v2/webhook-dispatches?limit=5

Get webhook dispatch

GET/v2/webhook-dispatches/{dispatchId}
.bash
curl -H "Authorization: Bearer $SCRAPELY_TOKEN" \
  https://api.scrape.ly/v2/webhook-dispatches/c8f2a6d4-3b1e-4f7a-a9c5-6e0d2b4f8a1c

Returns { "data": { ...dispatch } } with the same fields as a list item.

GET/v2/webhook-dispatches/c8f2a6d4-3b1e-4f7a-a9c5-6e0d2b4f8a1c